diff --git a/.agents/skills/add-block/SKILL.md b/.agents/skills/add-block/SKILL.md index 60778644da4..2361233e08a 100644 --- a/.agents/skills/add-block/SKILL.md +++ b/.agents/skills/add-block/SKILL.md @@ -927,6 +927,11 @@ bun run apps/sim/scripts/check-canvas-sentences.ts --block={service} ## Generated artifacts +When adding or changing `sunset.replacedBy`, run `bun run generate:block-successors` and commit +`apps/sim/lib/permission-groups/block-successors.generated.ts`. Authorization uses this generated +map to resolve legacy and current block IDs consistently without importing the executable registry. +Verify it with `bun run check:block-successors`. + Adding a block on its own needs no **tool metadata** regeneration — a block references existing tool IDs through `tools.access` and does not change any tool's shape. @@ -969,6 +974,7 @@ changes. - [ ] Tools.config.tool returns correct tool ID (snake_case) - [ ] Outputs match tool outputs - [ ] Block + meta registered in registry-maps.ts (`BLOCK_REGISTRY` / `BLOCK_META_REGISTRY`) +- [ ] If `sunset.replacedBy` changed: regenerated and committed the block successor map; `bun run check:block-successors` passes - [ ] If any tool was added, changed or removed alongside the block: ran `bun run tool-metadata:generate` and committed the artifacts - [ ] Ran `bun run scripts/generate-docs.ts`, reviewed the generated diff, and committed the integration catalog changes - [ ] `bun run integration-catalog:check` passes diff --git a/.agents/skills/add-tools/SKILL.md b/.agents/skills/add-tools/SKILL.md index e0f68e70c34..85d34b491cd 100644 --- a/.agents/skills/add-tools/SKILL.md +++ b/.agents/skills/add-tools/SKILL.md @@ -264,6 +264,38 @@ stale/missing sidecars, and scope isolation. ## Critical Rules for Outputs +### File Downloads and Generated Files + +Internal operations return `createInternalToolFileResult` / `createInternalToolFilesResult` from +`lib/internal/tool-operations/file-result.ts` with bounded Buffers and a callback that places the +stored descriptors in the response. Their handlers preserve this result through dispatch, using +`InternalToolOperationHandler`. Do not serialize file bytes as base64 +JSON: the executor's 10 MiB response cap runs before ordinary file postprocessing or large-value +externalization. The shared executor stores files using trusted run or Copilot ownership. + +External endpoints that return raw binary files explicitly declare `request.responseType: 'binary'` +and return `output.file` with `{ name, mimeType, data: buffer, size }` from `transformResponse`. +The executor applies the bounded file-transfer budget and persists the descriptor. This opt-in is +for raw binary responses, not provider JSON containing base64 or tools that fetch attachments later. +Keep provider-specific limits and bounded reads; a file declaration is not permission to enlarge +arbitrary JSON responses. + +Attachment readers that download files inside `transformResponse` need their own bounded reads: +the first response cap does not cover subsequent fetches. Accept `ToolResponseContext` as the third +transform argument, forward its `signal`, and share one `AttachmentDownloadBudget` across sequential +downloads. Prefer raw provider endpoints over base64 metadata. Return the same file object in the +declared `file` / `file[]` output and nested message associations; `FileToolProcessor` stores it once +and replaces every alias with the same `UserFile` in both workflow and Copilot execution. + +Preserve stored `UserFile` fields (`id`, `key`, `url`, `context`, `type`, `name`, `size`) in transforms; +rebuilding the old `{ name, mimeType, data, size }` shape discards the reference. File outputs do not +need duplicate inline text/base64 aliases; the file system handles content materialization. When +an existing tool explicitly exposes content aliases in its contract, preserve its legacy version and +use the existing block/tool version pattern for a file-only output. Test a file over 10 MiB through +executor admission, single persistence, trusted ownership, and the unchanged JSON cap. Avoid adding +top-level filename, size, MIME type, URL, or success fields that merely repeat the canonical file or +tool result; keep additional provider fields only when they convey distinct information. + ### Output Types - `'string'`, `'number'`, `'boolean'` - Primitives - `'json'` - Complex objects (use this, NOT 'object') diff --git a/.agents/skills/validate-integration/SKILL.md b/.agents/skills/validate-integration/SKILL.md index 308df1d6691..a32df2b8bb3 100644 --- a/.agents/skills/validate-integration/SKILL.md +++ b/.agents/skills/validate-integration/SKILL.md @@ -345,6 +345,21 @@ If any tool lists, searches, exports, imports, downloads, uploads, paginates, ba - [ ] List/search tools expose API limits and do not auto-fetch every page into memory - [ ] Transform logic does not build unbounded arrays, maps, sets, or `Promise.all` fan-outs - [ ] File and HTTP body reads use explicit byte caps or existing stream-limit helpers +- [ ] Internal file results reach `createInternalToolFileResult` / `createInternalToolFilesResult` + before JSON serialization; external raw downloads explicitly use `request.responseType: 'binary'` + and return a buffered `output.file`. Provider base64 JSON needs separate handling +- [ ] Transforms retain stored `UserFile` identity/access fields, and tests cover a >10 MiB file + crossing executor admission without another upload. New file outputs contain references only, + without inline content aliases; preserve legacy versions when removing existing inline fields +- [ ] Scan every file-producing path, including attachment fetches inside `transformResponse`, URL + descriptors, export operations, and old/new block versions; checking download-named tools alone + misses late reads that occur after the first response admission +- [ ] Late attachment reads share a per-call byte budget, bound actual streamed bytes independently + of provider size metadata, and forward `ToolResponseContext.signal` through every fetch/read +- [ ] Both workflow and Copilot tests produce compact `UserFile` outputs; nested message attachment + aliases reference the same stored files, with no duplicate upload or raw bytes left behind +- [ ] New output contracts omit redundant copies of file name, MIME type, size, URL, and success; + retained provider metadata has a distinct purpose, and types match the stored-file runtime shape - [ ] Large result payloads are summarized, paginated, referenced, or capped rather than raw-dumped - [ ] Pagination and download tests cover caps, early stop behavior, or partial-result preservation when relevant diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 51431ca589b..6178ca24e12 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -339,6 +339,7 @@ export const blockTypeToIconMap: Record = { azure_devops: AzureIcon, bitbucket: BitbucketIcon, box: BoxCompanyIcon, + box_v2: BoxCompanyIcon, brandfetch: BrandfetchIcon, brex: BrexIcon, brightdata: BrightDataIcon, @@ -380,9 +381,11 @@ export const blockTypeToIconMap: Record = { docusign: DocuSignIcon, downdetector: DowndetectorIcon, dropbox: DropboxIcon, + dropbox_v2: DropboxIcon, dropcontact: DropcontactIcon, dspy: DsPyIcon, dub: DubIcon, + dub_v2: DubIcon, duckduckgo: DuckDuckGoIcon, dynamodb: DynamoDBIcon, dynatrace: DynatraceIcon, @@ -475,6 +478,7 @@ export const blockTypeToIconMap: Record = { jotform: JotformIcon, jsm: JiraServiceManagementIcon, jupyter: JupyterIcon, + jupyter_v2: JupyterIcon, kalshi: KalshiIcon, kalshi_v2: KalshiIcon, ketch: KetchIcon, @@ -506,6 +510,7 @@ export const blockTypeToIconMap: Record = { memory: BrainIcon, microsoft_ad: AzureIcon, microsoft_dataverse: MicrosoftDataverseIcon, + microsoft_dataverse_v2: MicrosoftDataverseIcon, microsoft_dynamics_365: MicrosoftDataverseIcon, microsoft_excel: MicrosoftExcelIcon, microsoft_excel_v2: MicrosoftExcelIcon, @@ -557,6 +562,7 @@ export const blockTypeToIconMap: Record = { quartr: QuartrIcon, quickbooks: QuickBooksIcon, quiver: QuiverIcon, + quiver_v2: QuiverIcon, rabbitmq: RabbitmqIcon, railway: RailwayIcon, rb2b: RB2BIcon, @@ -588,8 +594,10 @@ export const blockTypeToIconMap: Record = { sentry: SentryIcon, serper: SerperIcon, servicenow: ServiceNowIcon, + servicenow_v2: ServiceNowIcon, ses: SESIcon, sftp: SftpIcon, + sftp_v2: SftpIcon, sharepoint: MicrosoftSharepointIcon, sharepoint_v2: MicrosoftSharepointIcon, shopify: ShopifyIcon, @@ -607,6 +615,7 @@ export const blockTypeToIconMap: Record = { sqs: SQSIcon, square: SquareIcon, ssh: SshIcon, + ssh_v2: SshIcon, ssm: SSMIcon, stagehand: StagehandIcon, start_trigger: StartIcon, diff --git a/apps/docs/content/docs/integrations/box.mdx b/apps/docs/content/docs/integrations/box.mdx index db628a6699d..f165d7f9058 100644 --- a/apps/docs/content/docs/integrations/box.mdx +++ b/apps/docs/content/docs/integrations/box.mdx @@ -6,7 +6,7 @@ description: Manage files, folders, and e-signatures with Box import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -63,7 +63,6 @@ Download a file from Box | Parameter | Type | Description | | --------- | ---- | ----------- | | `file` | file | Downloaded file stored in execution files | -| `content` | string | Base64 encoded file content | ### Box Get File Info diff --git a/apps/docs/content/docs/integrations/dropbox.mdx b/apps/docs/content/docs/integrations/dropbox.mdx index 27b95131829..ebd17a220a4 100644 --- a/apps/docs/content/docs/integrations/dropbox.mdx +++ b/apps/docs/content/docs/integrations/dropbox.mdx @@ -6,7 +6,7 @@ description: Upload, download, share, and manage files in Dropbox import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -66,7 +66,7 @@ Upload a file to Dropbox ### Dropbox Download File -Download a file from Dropbox with metadata and content +Download a file from Dropbox with metadata #### Input @@ -81,7 +81,6 @@ Download a file from Dropbox with metadata and content | `file` | file | Downloaded file stored in execution files | | `metadata` | json | The file metadata | | `temporaryLink` | string | Temporary link to download the file \(valid for ~4 hours\) | -| `content` | string | Base64 encoded file content \(if fetched\) | ### Dropbox List Folder diff --git a/apps/docs/content/docs/integrations/dub.mdx b/apps/docs/content/docs/integrations/dub.mdx index beb890bbf74..5ad7e6c23f1 100644 --- a/apps/docs/content/docs/integrations/dub.mdx +++ b/apps/docs/content/docs/integrations/dub.mdx @@ -6,7 +6,7 @@ description: Link management with Dub import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -474,7 +474,6 @@ Generate a customizable QR code (PNG) for a short link, with control over size, | Parameter | Type | Description | | --------- | ---- | ----------- | | `file` | file | Generated QR code image stored in execution files | -| `content` | string | Base64-encoded PNG image data | ### Dub List Domains diff --git a/apps/docs/content/docs/integrations/jupyter.mdx b/apps/docs/content/docs/integrations/jupyter.mdx index ceb5266a318..2025e5fc79f 100644 --- a/apps/docs/content/docs/integrations/jupyter.mdx +++ b/apps/docs/content/docs/integrations/jupyter.mdx @@ -6,7 +6,7 @@ description: Manage files, notebooks, kernels, and sessions on a Jupyter server import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -61,7 +61,7 @@ List files, notebooks, and subdirectories at a path on a Jupyter server ### Jupyter Get Content -Read a file or notebook from a Jupyter server +Download a file as a stored file, or read structured notebook and directory content #### Input @@ -75,11 +75,11 @@ Read a file or notebook from a Jupyter server | Parameter | Type | Description | | --------- | ---- | ----------- | -| `name` | string | File or notebook name | -| `path` | string | Path relative to the server root | -| `mimetype` | string | MIME type of the content | -| `text` | string | Text content, for text files and notebooks \(JSON-stringified\) | -| `file` | file | Binary content stored as a file, for base64-format content | +| `file` | file | Downloaded file | +| `text` | string | JSON-stringified notebook or directory content | +| `name` | string | Notebook or directory name | +| `path` | string | Notebook or directory path | +| `mimetype` | string | Notebook or directory MIME type | ### Jupyter Create File diff --git a/apps/docs/content/docs/integrations/microsoft_dataverse.mdx b/apps/docs/content/docs/integrations/microsoft_dataverse.mdx index 596dd1db9a7..91662cfea31 100644 --- a/apps/docs/content/docs/integrations/microsoft_dataverse.mdx +++ b/apps/docs/content/docs/integrations/microsoft_dataverse.mdx @@ -6,7 +6,7 @@ description: Manage records in Microsoft Dataverse tables import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -136,7 +136,7 @@ Remove an association between two records in Microsoft Dataverse. For collection ### Download File from Microsoft Dataverse -Download a file from a file or image column on a Dataverse record. Stores the file in execution storage and returns a file reference, plus the base64 content and metadata directly. +Download a file from a Dataverse file or image column and return its stored file reference and metadata #### Input @@ -152,12 +152,7 @@ Download a file from a file or image column on a Dataverse record. Stores the fi | Parameter | Type | Description | | --------- | ---- | ----------- | | `file` | file | Downloaded file stored in execution files | -| `fileContent` | string | Base64-encoded file content | -| `fileName` | string | Name of the downloaded file | -| `fileSize` | number | File size in bytes | -| `mimeType` | string | MIME type of the file | | `fileColumn` | string | File column the file was downloaded from | -| `success` | boolean | Whether the file was downloaded successfully | ### Execute Microsoft Dataverse Action diff --git a/apps/docs/content/docs/integrations/quiver.mdx b/apps/docs/content/docs/integrations/quiver.mdx index 282ca9aca7b..43a658acd42 100644 --- a/apps/docs/content/docs/integrations/quiver.mdx +++ b/apps/docs/content/docs/integrations/quiver.mdx @@ -6,7 +6,7 @@ description: Generate and vectorize SVGs import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -57,16 +57,12 @@ Generate SVG images from text prompts using QuiverAI | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Whether the SVG generation succeeded | -| `output` | object | Generated SVG output | -| ↳ `file` | file | First generated SVG file | -| ↳ `files` | json | All generated SVG files \(when n > 1\) | -| ↳ `svgContent` | string | Raw SVG markup content of the first result | -| ↳ `id` | string | Generation request ID | -| ↳ `usage` | json | Token usage statistics | -| ↳ `totalTokens` | number | Total tokens used | -| ↳ `inputTokens` | number | Input tokens used | -| ↳ `outputTokens` | number | Output tokens used | +| `files` | file[] | All generated SVG files | +| `id` | string | Request ID | +| `usage` | json | Token usage statistics | +| ↳ `totalTokens` | number | Total tokens used | +| ↳ `inputTokens` | number | Input tokens used | +| ↳ `outputTokens` | number | Output tokens used | ### Quiver Image to SVG @@ -90,15 +86,12 @@ Convert raster images into vector SVG format using QuiverAI | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Whether the vectorization succeeded | -| `output` | object | Vectorized SVG output | -| ↳ `file` | file | Generated SVG file | -| ↳ `svgContent` | string | Raw SVG markup content | -| ↳ `id` | string | Vectorization request ID | -| ↳ `usage` | json | Token usage statistics | -| ↳ `totalTokens` | number | Total tokens used | -| ↳ `inputTokens` | number | Input tokens used | -| ↳ `outputTokens` | number | Output tokens used | +| `files` | file[] | All generated SVG files | +| `id` | string | Request ID | +| `usage` | json | Token usage statistics | +| ↳ `totalTokens` | number | Total tokens used | +| ↳ `inputTokens` | number | Input tokens used | +| ↳ `outputTokens` | number | Output tokens used | ### Quiver List Models diff --git a/apps/docs/content/docs/integrations/servicenow.mdx b/apps/docs/content/docs/integrations/servicenow.mdx index 84482fba5fc..9fb36bf30f2 100644 --- a/apps/docs/content/docs/integrations/servicenow.mdx +++ b/apps/docs/content/docs/integrations/servicenow.mdx @@ -6,7 +6,7 @@ description: Create, read, update, and delete ServiceNow records import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -189,7 +189,6 @@ Download an attachment file from ServiceNow by its sys_id | Parameter | Type | Description | | --------- | ---- | ----------- | | `file` | file | Downloaded attachment stored in execution files | -| `content` | string | Base64 encoded file content | ### Upload ServiceNow Attachment @@ -258,7 +257,7 @@ Create an incident in ServiceNow. Reference fields (caller, assignment group, as | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -306,7 +305,7 @@ Retrieve a single ServiceNow incident by number (e.g., INC0010001) or sys_id. Re | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -362,7 +361,7 @@ Search ServiceNow incidents by state, priority, assignment, caller, or text. All | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -424,7 +423,7 @@ Update fields on an existing ServiceNow incident. Only the fields you supply are | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -476,7 +475,7 @@ Move a ServiceNow incident to Resolved (state 6) with a resolution code and reso | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -528,7 +527,7 @@ Move a ServiceNow incident to Closed (state 7) with a resolution code and resolu | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -578,7 +577,7 @@ Append an internal work note or a customer-visible additional comment to a Servi | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -643,7 +642,7 @@ Create a change request in ServiceNow. Reference fields (assignment group, assig | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -691,7 +690,7 @@ Retrieve a single ServiceNow change request by number (e.g., CHG0030001) or sys_ | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -747,7 +746,7 @@ Search ServiceNow change requests by state, type, risk, assignment, or text. All | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -809,7 +808,7 @@ Update fields on an existing ServiceNow change request. Only the fields you supp | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -862,7 +861,7 @@ Move a ServiceNow change request to another state. Base-system change model stat | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -1040,7 +1039,7 @@ List requested items (RITMs) from the ServiceNow Requested Item [sc_req_item] ta | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -1088,7 +1087,7 @@ Retrieve a single ServiceNow requested item (RITM) by number (e.g., RITM0010001) | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -1140,7 +1139,7 @@ List approval records from the ServiceNow Approval [sysapproval_approver] table. | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -1190,7 +1189,7 @@ Approve or reject a ServiceNow approval record by setting its state on the Appro | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -1242,7 +1241,7 @@ Search the ServiceNow CMDB for configuration items. Defaults to the base cmdb_ci | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -1324,7 +1323,7 @@ List rows from the CI Relationship [cmdb_rel_ci] table for a configuration item. | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -1444,7 +1443,7 @@ Look up ServiceNow users by email, user name, or display name. Use this to resol | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | @@ -1495,7 +1494,7 @@ List the members of a ServiceNow group from the Group Member [sys_user_grmember] | `count` | number | Aggregate matching record count | | `attachments` | json | Attachment metadata list — record attachments \(sys_id, file_name, content_type, download_link\) or knowledge article attachments \(sys_id, file_name, size_bytes, state\) | | `file` | file | Downloaded attachment file | -| `content` | string | Base64-encoded downloaded file content, or the HTML body of a knowledge article | +| `content` | string | HTML body of a knowledge article | | `attachment` | json | Uploaded attachment metadata | | `tasks` | json | Change tasks belonging to a change request. Unlike the Table API operations, the Change Management API always returns every field as \{value, display_value\} regardless of the display value setting | | `items` | json | Service catalog items | diff --git a/apps/docs/content/docs/integrations/sftp.mdx b/apps/docs/content/docs/integrations/sftp.mdx index 60b2397b18a..a62e82ca437 100644 --- a/apps/docs/content/docs/integrations/sftp.mdx +++ b/apps/docs/content/docs/integrations/sftp.mdx @@ -6,7 +6,7 @@ description: Transfer files via SFTP (SSH File Transfer Protocol) import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -77,19 +77,12 @@ Download a file from a remote SFTP server | `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | | `passphrase` | string | No | Passphrase for encrypted private key | | `remotePath` | string | Yes | Path to the file on the remote server | -| `encoding` | string | No | Output encoding: utf-8 for text, base64 for binary \(default: utf-8\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Whether the download was successful | | `file` | file | Downloaded file stored in execution files | -| `fileName` | string | Name of the downloaded file | -| `content` | string | File content \(text or base64 encoded\) | -| `size` | number | File size in bytes | -| `encoding` | string | Content encoding \(utf-8 or base64\) | -| `message` | string | Operation status message | ### SFTP List Directory diff --git a/apps/docs/content/docs/integrations/ssh.mdx b/apps/docs/content/docs/integrations/ssh.mdx index 3b6e7631df7..23e16880e0e 100644 --- a/apps/docs/content/docs/integrations/ssh.mdx +++ b/apps/docs/content/docs/integrations/ssh.mdx @@ -6,7 +6,7 @@ description: Connect to remote servers via SSH import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -161,13 +161,8 @@ Download a file from a remote SSH server | Parameter | Type | Description | | --------- | ---- | ----------- | -| `downloaded` | boolean | Whether the file was downloaded successfully | | `file` | file | Downloaded file stored in execution files | -| `fileContent` | string | File content \(base64 encoded for binary files\) | -| `fileName` | string | Name of the downloaded file | | `remotePath` | string | Source path on the remote server | -| `size` | number | File size in bytes | -| `message` | string | Operation status message | ### SSH List Directory diff --git a/apps/sim/blocks/blocks/binary-download-versioning.test.ts b/apps/sim/blocks/blocks/binary-download-versioning.test.ts new file mode 100644 index 00000000000..88a5fe12b7f --- /dev/null +++ b/apps/sim/blocks/blocks/binary-download-versioning.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from 'vitest' +import { BoxBlock, BoxV2Block } from '@/blocks/blocks/box' +import { DropboxBlock, DropboxV2Block } from '@/blocks/blocks/dropbox' +import { DubBlock, DubV2Block } from '@/blocks/blocks/dub' +import { + MicrosoftDataverseBlock, + MicrosoftDataverseV2Block, +} from '@/blocks/blocks/microsoft_dataverse' +import { ServiceNowBlock, ServiceNowV2Block } from '@/blocks/blocks/servicenow' +import type { BlockConfig } from '@/blocks/types' + +const cases: { + legacy: BlockConfig + current: BlockConfig + operation: string + toolId: string + content: string +}[] = [ + { + legacy: BoxBlock, + current: BoxV2Block, + operation: 'download_file', + toolId: 'box_download_file', + content: 'content', + }, + { + legacy: DropboxBlock, + current: DropboxV2Block, + operation: 'dropbox_download', + toolId: 'dropbox_download', + content: 'content', + }, + { + legacy: DubBlock, + current: DubV2Block, + operation: 'get_qr_code', + toolId: 'dub_get_qr_code', + content: 'content', + }, + { + legacy: MicrosoftDataverseBlock, + current: MicrosoftDataverseV2Block, + operation: 'download_file', + toolId: 'microsoft_dataverse_download_file', + content: 'fileContent', + }, + { + legacy: ServiceNowBlock, + current: ServiceNowV2Block, + operation: 'servicenow_download_attachment', + toolId: 'servicenow_download_attachment', + content: 'content', + }, +] + +describe.each(cases)( + '$current.type download versioning', + ({ legacy, current, operation, toolId, content }) => { + it('preserves saved blocks and changes only the download tool for new blocks', () => { + expect(legacy.hideFromToolbar).toBe(true) + expect(legacy.sunset).toEqual({ status: 'legacy', replacedBy: current.type }) + expect(current.hideFromToolbar).toBe(false) + expect(current.sunset).toBeUndefined() + expect(current.subBlocks).toBe(legacy.subBlocks) + expect(current.tools.config?.params).toBe(legacy.tools.config?.params) + expect(legacy.tools.config?.tool({ operation })).toBe(toolId) + expect(current.tools.config?.tool({ operation })).toBe(`${toolId}_v2`) + expect(current.tools.access).toEqual( + legacy.tools.access.map((id) => (id === toolId ? `${id}_v2` : id)) + ) + }) + + it('removes the download content output while preserving unrelated outputs', () => { + expect(legacy.outputs).toHaveProperty(content) + if (current.type === 'servicenow_v2') { + expect(current.outputs.content).toEqual({ + type: 'string', + description: 'HTML body of a knowledge article', + }) + } else { + expect(current.outputs).not.toHaveProperty(content) + } + expect(current.outputs.file).toEqual(legacy.outputs.file) + }) + } +) + +it('keeps Dataverse upload metadata separate from canonical download files', () => { + expect(MicrosoftDataverseV2Block.outputs).not.toHaveProperty('fileSize') + expect(MicrosoftDataverseV2Block.outputs).not.toHaveProperty('mimeType') + expect(MicrosoftDataverseV2Block.outputs.fileName.condition).toEqual({ + field: 'operation', + value: 'upload_file', + }) + expect(MicrosoftDataverseV2Block.outputs.success.condition).toEqual({ + field: 'operation', + value: 'download_file', + not: true, + }) +}) diff --git a/apps/sim/blocks/blocks/box.ts b/apps/sim/blocks/blocks/box.ts index 6d3b03f157e..3b81fc5a25b 100644 --- a/apps/sim/blocks/blocks/box.ts +++ b/apps/sim/blocks/blocks/box.ts @@ -1,3 +1,4 @@ +import { omit } from '@sim/utils/object' import { BoxCompanyIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' @@ -7,9 +8,11 @@ import { normalizeFileInput } from '@/blocks/utils' /** Canonical pair for the upload payload: file picker in basic mode, file reference in advanced. */ const UPLOAD_FILE_FIELD = ['uploadFile', 'fileRef'] as const -export const BoxBlock: BlockConfig = { +export const BoxBlock = { type: 'box', - name: 'Box', + name: 'Box (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'box_v2' }, description: 'Manage files, folders, and e-signatures with Box', longDescription: 'Integrate Box into your workflow to manage files, folders, and e-signatures. Upload and download files, search content, create folders, send documents for e-signature, track signing status, and more.', @@ -657,6 +660,28 @@ export const BoxBlock: BlockConfig = { count: 'number', nextMarker: 'string', }, +} satisfies BlockConfig + +export const BoxV2Block: BlockConfig = { + ...BoxBlock, + type: 'box_v2', + name: 'Box', + hideFromToolbar: false, + sunset: undefined, + tools: { + ...BoxBlock.tools, + access: BoxBlock.tools.access.map((toolId) => + toolId === 'box_download_file' ? 'box_download_file_v2' : toolId + ), + config: { + ...BoxBlock.tools.config, + tool: (params) => { + const toolId = BoxBlock.tools.config.tool(params) + return toolId === 'box_download_file' ? 'box_download_file_v2' : toolId + }, + }, + }, + outputs: omit(BoxBlock.outputs, ['content']), } export const BoxBlockMeta = { diff --git a/apps/sim/blocks/blocks/dropbox.ts b/apps/sim/blocks/blocks/dropbox.ts index 3047df56821..572c47047d0 100644 --- a/apps/sim/blocks/blocks/dropbox.ts +++ b/apps/sim/blocks/blocks/dropbox.ts @@ -1,3 +1,4 @@ +import { omit } from '@sim/utils/object' import { DropboxIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' @@ -12,9 +13,11 @@ import type { DropboxResponse } from '@/tools/dropbox/types' */ const UPLOAD_FILE_FIELD = ['uploadFile', 'fileRef'] as const -export const DropboxBlock: BlockConfig = { +export const DropboxBlock = { type: 'dropbox', - name: 'Dropbox', + name: 'Dropbox (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'dropbox_v2' }, description: 'Upload, download, share, and manage files in Dropbox', authMode: AuthMode.OAuth, longDescription: @@ -544,6 +547,28 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, // List revisions output isDeleted: { type: 'boolean', description: 'Whether the latest revision is deleted or moved' }, }, +} satisfies BlockConfig + +export const DropboxV2Block: BlockConfig = { + ...DropboxBlock, + type: 'dropbox_v2', + name: 'Dropbox', + hideFromToolbar: false, + sunset: undefined, + tools: { + ...DropboxBlock.tools, + access: DropboxBlock.tools.access.map((toolId) => + toolId === 'dropbox_download' ? 'dropbox_download_v2' : toolId + ), + config: { + ...DropboxBlock.tools.config, + tool: (params) => { + const toolId = DropboxBlock.tools.config.tool(params) + return toolId === 'dropbox_download' ? 'dropbox_download_v2' : toolId + }, + }, + }, + outputs: omit(DropboxBlock.outputs, ['content']), } export const DropboxBlockMeta = { diff --git a/apps/sim/blocks/blocks/dub.ts b/apps/sim/blocks/blocks/dub.ts index 1b2e213be6a..3361f171254 100644 --- a/apps/sim/blocks/blocks/dub.ts +++ b/apps/sim/blocks/blocks/dub.ts @@ -1,3 +1,4 @@ +import { omit } from '@sim/utils/object' import { DubIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' @@ -8,9 +9,11 @@ const BULK_UPDATE_TARGET_FIELD = ['bulkUpdateLinkIds', 'bulkUpdateExternalIds'] const ANALYTICS_LINK_FIELD = ['analyticsLinkId', 'analyticsExternalId'] as const const EVENTS_LINK_FIELD = ['eventsLinkId', 'eventsExternalId'] as const -export const DubBlock: BlockConfig = { +export const DubBlock = { type: 'dub', - name: 'Dub', + name: 'Dub (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'dub_v2' }, description: 'Link management with Dub', authMode: AuthMode.ApiKey, longDescription: @@ -1277,6 +1280,28 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'create_tag' }, }, }, +} satisfies BlockConfig + +export const DubV2Block: BlockConfig = { + ...DubBlock, + type: 'dub_v2', + name: 'Dub', + hideFromToolbar: false, + sunset: undefined, + tools: { + ...DubBlock.tools, + access: DubBlock.tools.access.map((toolId) => + toolId === 'dub_get_qr_code' ? 'dub_get_qr_code_v2' : toolId + ), + config: { + ...DubBlock.tools.config, + tool: (params) => { + const toolId = DubBlock.tools.config.tool(params) + return toolId === 'dub_get_qr_code' ? 'dub_get_qr_code_v2' : toolId + }, + }, + }, + outputs: omit(DubBlock.outputs, ['content']), } export const DubBlockMeta = { diff --git a/apps/sim/blocks/blocks/jupyter.test.ts b/apps/sim/blocks/blocks/jupyter.test.ts new file mode 100644 index 00000000000..5c5c284b7e9 --- /dev/null +++ b/apps/sim/blocks/blocks/jupyter.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { JupyterBlock, JupyterV2Block } from '@/blocks/blocks/jupyter' + +describe('Jupyter block versions', () => { + it('offers v2 for new blocks and preserves the legacy block', () => { + expect(JupyterBlock.hideFromToolbar).toBe(true) + expect(JupyterBlock.sunset).toEqual({ status: 'legacy', replacedBy: 'jupyter_v2' }) + expect(JupyterV2Block.hideFromToolbar).toBe(false) + expect(JupyterV2Block.sunset).toBeUndefined() + expect(JupyterV2Block.canvasPresentation).toBe(JupyterBlock.canvasPresentation) + expect(JupyterV2Block.subBlocks).toBe(JupyterBlock.subBlocks) + }) + + it.each(JupyterBlock.tools.access)('versions only Get Content: %s', (operation) => { + const id = operation === 'jupyter_get_content' ? 'jupyter_get_content_v2' : operation + expect(JupyterBlock.tools.config.tool({ operation })).toBe(operation) + expect(JupyterV2Block.tools.config?.tool({ operation })).toBe(id) + expect(JupyterV2Block.tools.access).toContain(id) + }) + + it('preserves the path and credentials for the versioned read', () => { + expect( + JupyterV2Block.tools.config?.params?.({ + operation: 'jupyter_get_content', + serverUrl: 'https://jupyter.example.com', + token: 'token', + path: 'reports/book.xlsx', + }) + ).toEqual({ + serverUrl: 'https://jupyter.example.com', + token: 'token', + path: 'reports/book.xlsx', + }) + }) +}) diff --git a/apps/sim/blocks/blocks/jupyter.ts b/apps/sim/blocks/blocks/jupyter.ts index e251c738abc..55d6c0dd03c 100644 --- a/apps/sim/blocks/blocks/jupyter.ts +++ b/apps/sim/blocks/blocks/jupyter.ts @@ -1,7 +1,7 @@ import { JupyterIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' -import { normalizeFileInput } from '@/blocks/utils' +import { createVersionedToolSelector, normalizeFileInput } from '@/blocks/utils' const PATH_OPERATIONS = [ 'jupyter_list_contents', @@ -31,9 +31,11 @@ const KERNEL_ID_OPERATIONS = [ /** Both members of the `file` canonical group — advanced mode fills only `fileRef`. */ const UPLOAD_FILE_FIELD = ['uploadFile', 'fileRef'] as const -export const JupyterBlock: BlockConfig = { +export const JupyterBlock = { type: 'jupyter', - name: 'Jupyter', + name: 'Jupyter (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'jupyter_v2' }, description: 'Manage files, notebooks, kernels, and sessions on a Jupyter server', longDescription: 'Integrate a self-hosted Jupyter server into the workflow. Browse, read, create, upload, rename, copy, and delete files and notebooks; start, stop, restart, and interrupt kernels; and manage sessions that bind notebooks to kernels.', @@ -392,6 +394,33 @@ export const JupyterBlock: BlockConfig = { kernelId: 'string', sessionId: 'string', }, +} satisfies BlockConfig + +const selectJupyterV2Tool = createVersionedToolSelector({ + baseToolSelector: JupyterBlock.tools.config.tool, + suffix: '_v2', + fallbackToolId: 'jupyter_get_content_v2', +}) + +export const JupyterV2Block: BlockConfig = { + ...JupyterBlock, + type: 'jupyter_v2', + name: 'Jupyter', + hideFromToolbar: false, + sunset: undefined, + tools: { + ...JupyterBlock.tools, + access: JupyterBlock.tools.access.map((toolId) => + toolId === 'jupyter_get_content' ? 'jupyter_get_content_v2' : toolId + ), + config: { + ...JupyterBlock.tools.config, + tool: (params) => + params.operation === 'jupyter_get_content' + ? selectJupyterV2Tool(params) + : JupyterBlock.tools.config.tool(params), + }, + }, } export const JupyterBlockMeta = { diff --git a/apps/sim/blocks/blocks/microsoft_dataverse.ts b/apps/sim/blocks/blocks/microsoft_dataverse.ts index 2c51cfe7ad2..748859694a0 100644 --- a/apps/sim/blocks/blocks/microsoft_dataverse.ts +++ b/apps/sim/blocks/blocks/microsoft_dataverse.ts @@ -1,3 +1,4 @@ +import { omit } from '@sim/utils/object' import { MicrosoftDataverseIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' @@ -8,9 +9,11 @@ import type { DataverseResponse } from '@/tools/microsoft_dataverse/types' /** Canonical upload pair for the file column payload, basic then advanced. */ const FILE_FIELD = ['uploadFile', 'fileReference'] as const -export const MicrosoftDataverseBlock: BlockConfig = { +export const MicrosoftDataverseBlock = { type: 'microsoft_dataverse', - name: 'Microsoft Dataverse', + name: 'Microsoft Dataverse (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'microsoft_dataverse_v2' }, description: 'Manage records in Microsoft Dataverse tables', authMode: AuthMode.OAuth, longDescription: @@ -788,6 +791,43 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, description: 'Full raw table metadata response (get table metadata)', }, }, +} satisfies BlockConfig + +export const MicrosoftDataverseV2Block: BlockConfig = { + ...MicrosoftDataverseBlock, + type: 'microsoft_dataverse_v2', + name: 'Microsoft Dataverse', + hideFromToolbar: false, + sunset: undefined, + tools: { + ...MicrosoftDataverseBlock.tools, + access: MicrosoftDataverseBlock.tools.access.map((toolId) => + toolId === 'microsoft_dataverse_download_file' + ? 'microsoft_dataverse_download_file_v2' + : toolId + ), + config: { + ...MicrosoftDataverseBlock.tools.config, + tool: (params) => { + const toolId = MicrosoftDataverseBlock.tools.config.tool(params) + return toolId === 'microsoft_dataverse_download_file' + ? 'microsoft_dataverse_download_file_v2' + : toolId + }, + }, + }, + outputs: { + ...omit(MicrosoftDataverseBlock.outputs, ['fileContent', 'fileSize', 'mimeType']), + fileName: { + type: 'string', + description: 'Name of the uploaded file', + condition: { field: 'operation', value: 'upload_file' }, + }, + success: { + ...MicrosoftDataverseBlock.outputs.success, + condition: { field: 'operation', value: 'download_file', not: true }, + }, + }, } export const MicrosoftDataverseBlockMeta = { diff --git a/apps/sim/blocks/blocks/quiver.test.ts b/apps/sim/blocks/blocks/quiver.test.ts new file mode 100644 index 00000000000..c1953d8dfc6 --- /dev/null +++ b/apps/sim/blocks/blocks/quiver.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { QuiverBlock, QuiverV2Block } from '@/blocks/blocks/quiver' + +describe('Quiver block versions', () => { + it('keeps the legacy block executable and offers v2 for new blocks', () => { + expect(QuiverBlock.hideFromToolbar).toBe(true) + expect(QuiverBlock.sunset).toEqual({ status: 'legacy', replacedBy: 'quiver_v2' }) + expect(QuiverV2Block.name).toBe('Quiver') + expect(QuiverV2Block.hideFromToolbar).toBe(false) + expect(QuiverV2Block.sunset).toBeUndefined() + expect(QuiverV2Block.subBlocks).toBe(QuiverBlock.subBlocks) + expect(QuiverV2Block.tools.config?.params).toBe(QuiverBlock.tools.config?.params) + }) + + it.each([ + ['text_to_svg', 'quiver_text_to_svg', 'quiver_text_to_svg_v2'], + ['image_to_svg', 'quiver_image_to_svg', 'quiver_image_to_svg_v2'], + ['list_models', 'quiver_list_models', 'quiver_list_models'], + ])('routes %s to the versioned response contract', (operation, legacyId, currentId) => { + expect(QuiverBlock.tools.config?.tool({ operation })).toBe(legacyId) + expect(QuiverV2Block.tools.config?.tool({ operation })).toBe(currentId) + expect(QuiverBlock.tools.access).toContain(legacyId) + expect(QuiverV2Block.tools.access).toContain(currentId) + }) + + it('defaults to SVG generation and exposes all generated files in v2', () => { + expect(QuiverV2Block.tools.config?.tool({})).toBe('quiver_text_to_svg_v2') + expect(QuiverV2Block.outputs.files).toMatchObject({ type: 'file[]' }) + expect(QuiverBlock.outputs.files).toMatchObject({ type: 'json' }) + expect(QuiverV2Block.outputs).not.toHaveProperty('svgContent') + expect(QuiverV2Block.outputs).not.toHaveProperty('file') + expect(QuiverBlock.outputs.svgContent).toMatchObject({ type: 'string' }) + }) +}) diff --git a/apps/sim/blocks/blocks/quiver.ts b/apps/sim/blocks/blocks/quiver.ts index 879c9fa90df..c87e691642a 100644 --- a/apps/sim/blocks/blocks/quiver.ts +++ b/apps/sim/blocks/blocks/quiver.ts @@ -1,15 +1,18 @@ +import { omit } from '@sim/utils/object' import { QuiverIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' -import { normalizeFileInput } from '@/blocks/utils' -import type { QuiverSvgResponse } from '@/tools/quiver/types' +import { createVersionedToolSelector, normalizeFileInput } from '@/blocks/utils' +import type { QuiverSvgResponse, QuiverSvgV2Response } from '@/tools/quiver/types' const REFERENCE_IMAGES_FIELD = ['referenceFiles', 'referenceInput'] as const const IMAGE_FIELD = ['imageFile', 'imageInput'] as const -export const QuiverBlock: BlockConfig = { +export const QuiverBlock = { type: 'quiver', - name: 'Quiver', + name: 'Quiver (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'quiver_v2' }, description: 'Generate and vectorize SVGs', longDescription: 'Generate SVG images from text prompts or vectorize raster images into SVGs using QuiverAI. Supports reference images, style instructions, and multiple output generation.', @@ -255,6 +258,34 @@ export const QuiverBlock: BlockConfig = { description: 'List of available models (list_models operation only)', }, }, +} satisfies BlockConfig + +const selectQuiverV2Tool = createVersionedToolSelector({ + baseToolSelector: (params: Record) => + `quiver_${params.operation || 'text_to_svg'}`, + suffix: '_v2', + fallbackToolId: 'quiver_text_to_svg_v2', +}) + +export const QuiverV2Block: BlockConfig = { + ...QuiverBlock, + type: 'quiver_v2', + name: 'Quiver', + hideFromToolbar: false, + sunset: undefined, + tools: { + ...QuiverBlock.tools, + access: ['quiver_text_to_svg_v2', 'quiver_image_to_svg_v2', 'quiver_list_models'], + config: { + ...QuiverBlock.tools.config, + tool: (params: Record) => + params.operation === 'list_models' ? 'quiver_list_models' : selectQuiverV2Tool(params), + }, + }, + outputs: { + ...omit(QuiverBlock.outputs, ['file', 'svgContent']), + files: { type: 'file[]', description: 'All generated SVG files' }, + }, } export const QuiverBlockMeta = { @@ -294,13 +325,13 @@ export const QuiverBlockMeta = { name: 'generate-brand-icon', description: 'Generate a clean SVG icon from a text prompt and save it to the files store.', content: - '# Generate Brand Icon\n\nTurn a text description into a production-ready SVG icon using Quiver text-to-SVG.\n\n## Steps\n1. Collect the icon concept (for example, a product name plus a brand color and style cues).\n2. Run the text_to_svg operation with a focused prompt that names the subject, color palette, and visual style (flat, line, filled).\n3. Optionally set n greater than 1 to generate several variations to choose from.\n4. Save the returned SVG file to the files store, or pass svgContent downstream for embedding.\n\n## Output\nReport the saved file location and the request id. When multiple variations are generated, list each so the user can pick one.', + '# Generate Brand Icon\n\nTurn a text description into a production-ready SVG icon using Quiver text-to-SVG.\n\n## Steps\n1. Collect the icon concept (for example, a product name plus a brand color and style cues).\n2. Run the text_to_svg operation with a focused prompt that names the subject, color palette, and visual style (flat, line, filled).\n3. Optionally set n greater than 1 to generate several variations to choose from.\n4. Save the returned SVG file to the files store, or pass the file downstream.\n\n## Output\nReport the saved file location and the request id. When multiple variations are generated, list each so the user can pick one.', }, { name: 'vectorize-raster-image', description: 'Convert an uploaded raster image (PNG or JPG) into a clean editable SVG.', content: - '# Vectorize Raster Image\n\nConvert a bitmap logo or graphic into a scalable SVG with Quiver image-to-SVG.\n\n## Steps\n1. Accept the raster image upload and pass it as the image input.\n2. Run the image_to_svg operation, optionally setting auto_crop and a target_size to tighten the output.\n3. Inspect svgContent for fidelity; rerun with adjusted instructions if details are lost.\n4. Save the SVG file for use in presentations, exports, or the web.\n\n## Output\nReturn the vectorized SVG file and confirm dimensions. Note any visual elements that did not vectorize cleanly.', + '# Vectorize Raster Image\n\nConvert a bitmap logo or graphic into a scalable SVG with Quiver image-to-SVG.\n\n## Steps\n1. Accept the raster image upload and pass it as the image input.\n2. Run the image_to_svg operation, optionally setting auto_crop and a target_size to tighten the output.\n3. Inspect the generated SVG file for fidelity; rerun with adjusted instructions if details are lost.\n4. Save the SVG file for use in presentations, exports, or the web.\n\n## Output\nReturn the vectorized SVG file and confirm dimensions. Note any visual elements that did not vectorize cleanly.', }, { name: 'create-data-diagram', diff --git a/apps/sim/blocks/blocks/servicenow.ts b/apps/sim/blocks/blocks/servicenow.ts index 4b4eb33cac9..6ac69083260 100644 --- a/apps/sim/blocks/blocks/servicenow.ts +++ b/apps/sim/blocks/blocks/servicenow.ts @@ -162,9 +162,11 @@ const optionalChoices = (options: reado ...options, ] -export const ServiceNowBlock: BlockConfig = { +export const ServiceNowBlock = { type: 'servicenow', - name: 'ServiceNow', + name: 'ServiceNow (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'servicenow_v2' }, description: 'Create, read, update, and delete ServiceNow records', longDescription: 'Integrate ServiceNow into your workflow. Create, read, update, and delete records in any ServiceNow table including incidents, tasks, change requests, users, and more.', @@ -1921,6 +1923,33 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st 'servicenow_webhook', ], }, +} satisfies BlockConfig + +export const ServiceNowV2Block: BlockConfig = { + ...ServiceNowBlock, + type: 'servicenow_v2', + name: 'ServiceNow', + hideFromToolbar: false, + sunset: undefined, + tools: { + ...ServiceNowBlock.tools, + access: ServiceNowBlock.tools.access.map((toolId) => + toolId === 'servicenow_download_attachment' ? 'servicenow_download_attachment_v2' : toolId + ), + config: { + ...ServiceNowBlock.tools.config, + tool: (params) => { + const toolId = ServiceNowBlock.tools.config.tool(params) + return toolId === 'servicenow_download_attachment' + ? 'servicenow_download_attachment_v2' + : toolId + }, + }, + }, + outputs: { + ...ServiceNowBlock.outputs, + content: { type: 'string', description: 'HTML body of a knowledge article' }, + }, } export const ServiceNowBlockMeta = { diff --git a/apps/sim/blocks/blocks/sftp.test.ts b/apps/sim/blocks/blocks/sftp.test.ts new file mode 100644 index 00000000000..6d8570a2308 --- /dev/null +++ b/apps/sim/blocks/blocks/sftp.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' +import { SftpBlock, SftpV2Block } from '@/blocks/blocks/sftp' + +describe('SFTP block versions', () => { + it('preserves legacy blocks and offers v2 for new blocks', () => { + expect(SftpBlock.hideFromToolbar).toBe(true) + expect(SftpBlock.sunset).toEqual({ status: 'legacy', replacedBy: 'sftp_v2' }) + expect(SftpV2Block.hideFromToolbar).toBe(false) + expect(SftpV2Block.sunset).toBeUndefined() + expect(SftpV2Block.name).toBe('SFTP') + expect(SftpV2Block.canvasPresentation).toBe(SftpBlock.canvasPresentation) + }) + + it.each(SftpBlock.tools.access)('versions only the download operation: %s', (operation) => { + const currentId = operation === 'sftp_download' ? 'sftp_download_v2' : operation + expect(SftpBlock.tools.config.tool({ operation })).toBe(operation) + expect(SftpV2Block.tools.config?.tool({ operation })).toBe(currentId) + expect(SftpV2Block.tools.access).toContain(currentId) + }) + + it('preserves defaults and the create-file alias', () => { + expect(SftpV2Block.tools.config?.tool({})).toBe('sftp_upload') + expect(SftpV2Block.tools.config?.tool({ operation: 'sftp_create' })).toBe('sftp_upload') + }) + + it('removes download encoding and inline content from v2', () => { + expect(SftpBlock.subBlocks.some((subBlock) => subBlock.id === 'encoding')).toBe(true) + expect(SftpV2Block.subBlocks.some((subBlock) => subBlock.id === 'encoding')).toBe(false) + expect(SftpV2Block.inputs).not.toHaveProperty('encoding') + expect(SftpV2Block.outputs).not.toHaveProperty('content') + expect(SftpV2Block.outputs).not.toHaveProperty('fileName') + expect(SftpV2Block.outputs).not.toHaveProperty('size') + for (const key of ['success', 'message']) { + expect(SftpV2Block.outputs[key].condition).toEqual({ + field: 'operation', + value: 'sftp_download', + not: true, + }) + } + expect(SftpV2Block.outputs.file.type).toBe('file') + const params = { + operation: 'sftp_download', + host: 'sftp.example.com', + port: '22', + username: 'user', + password: 'test-password', + remotePath: '/file.txt', + encoding: 'base64', + } + expect(SftpBlock.tools.config.params(params)).toHaveProperty('encoding', 'base64') + expect(SftpV2Block.tools.config?.params?.(params)).toEqual({ + host: 'sftp.example.com', + port: 22, + username: 'user', + password: 'test-password', + remotePath: '/file.txt', + }) + }) +}) diff --git a/apps/sim/blocks/blocks/sftp.ts b/apps/sim/blocks/blocks/sftp.ts index 62181bdaabb..a4132d27565 100644 --- a/apps/sim/blocks/blocks/sftp.ts +++ b/apps/sim/blocks/blocks/sftp.ts @@ -1,13 +1,16 @@ import { ClipboardList, Download, File, Search, Server, Trash, Upload } from '@sim/emcn/icons' +import { omit } from '@sim/utils/object' import { SftpIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' -import { normalizeFileInput } from '@/blocks/utils' +import { createVersionedToolSelector, normalizeFileInput } from '@/blocks/utils' import type { SftpUploadResult } from '@/tools/sftp/types' -export const SftpBlock: BlockConfig = { +export const SftpBlock = { type: 'sftp', - name: 'SFTP', + name: 'SFTP (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'sftp_v2' }, description: 'Transfer files via SFTP (SSH File Transfer Protocol)', longDescription: 'Upload, download, list, and manage files on remote servers via SFTP. Supports both password and private key authentication for secure file transfers.', @@ -332,6 +335,50 @@ export const SftpBlock: BlockConfig = { message: { type: 'string', description: 'Operation status message' }, error: { type: 'string', description: 'Error message if operation failed' }, }, +} satisfies BlockConfig + +const selectSftpV2Tool = createVersionedToolSelector({ + baseToolSelector: SftpBlock.tools.config.tool, + suffix: '_v2', + fallbackToolId: 'sftp_download_v2', +}) + +export const SftpV2Block: BlockConfig = { + ...SftpBlock, + type: 'sftp_v2', + name: 'SFTP', + hideFromToolbar: false, + sunset: undefined, + subBlocks: SftpBlock.subBlocks.filter((subBlock) => subBlock.id !== 'encoding'), + tools: { + ...SftpBlock.tools, + access: SftpBlock.tools.access.map((toolId) => + toolId === 'sftp_download' ? 'sftp_download_v2' : toolId + ), + config: { + ...SftpBlock.tools.config, + tool: (params) => + params.operation === 'sftp_download' + ? selectSftpV2Tool(params) + : SftpBlock.tools.config.tool(params), + params: (params) => { + const input: Record = SftpBlock.tools.config.params(params) + return omit(input, ['encoding']) + }, + }, + }, + inputs: omit(SftpBlock.inputs, ['encoding']), + outputs: { + ...omit(SftpBlock.outputs, ['content', 'fileName', 'size']), + success: { + ...SftpBlock.outputs.success, + condition: { field: 'operation', value: 'sftp_download', not: true }, + }, + message: { + ...SftpBlock.outputs.message, + condition: { field: 'operation', value: 'sftp_download', not: true }, + }, + }, } export const SftpBlockMeta = { @@ -416,7 +463,7 @@ export const SftpBlockMeta = { name: 'pull-remote-drop-folder', description: 'Poll a remote SFTP drop folder on a schedule and ingest any new files.', content: - '# Pull Remote Drop Folder\n\nPeriodically fetch newly arrived files from a remote SFTP directory into a workflow.\n\n## Steps\n1. Use the List Directory operation to read the remote drop folder and inspect `entries`.\n2. Filter for files newer than the last processed timestamp.\n3. For each new file, use the Download File operation and read `file`/`content`.\n4. Hand the contents to downstream blocks for parsing.\n\n## Output\nNew remote files are downloaded and their contents are available for processing each run.', + '# Pull Remote Drop Folder\n\nPeriodically fetch newly arrived files from a remote SFTP directory into a workflow.\n\n## Steps\n1. Use the List Directory operation to read the remote drop folder and inspect `entries`.\n2. Filter for files newer than the last processed timestamp.\n3. For each new file, use the Download File operation and read `file`.\n4. Pass the file to downstream blocks for parsing.\n\n## Output\nNew remote files are downloaded and available for processing each run.', }, { name: 'push-report-to-partner', diff --git a/apps/sim/blocks/blocks/ssh.test.ts b/apps/sim/blocks/blocks/ssh.test.ts new file mode 100644 index 00000000000..a49753c3c08 --- /dev/null +++ b/apps/sim/blocks/blocks/ssh.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import { SSHBlock, SSHV2Block } from '@/blocks/blocks/ssh' + +describe('SSH block versions', () => { + it('preserves legacy blocks and offers v2 for new blocks', () => { + expect(SSHBlock.hideFromToolbar).toBe(true) + expect(SSHBlock.sunset).toEqual({ status: 'legacy', replacedBy: 'ssh_v2' }) + expect(SSHV2Block.hideFromToolbar).toBe(false) + expect(SSHV2Block.sunset).toBeUndefined() + expect(SSHV2Block.name).toBe('SSH') + expect(SSHV2Block.subBlocks).toBe(SSHBlock.subBlocks) + expect(SSHV2Block.tools.config?.params).toBe(SSHBlock.tools.config.params) + expect(SSHV2Block.canvasPresentation).toBe(SSHBlock.canvasPresentation) + }) + + it.each(SSHBlock.tools.access)('versions only the download operation: %s', (operation) => { + const currentId = operation === 'ssh_download_file' ? 'ssh_download_file_v2' : operation + expect(SSHBlock.tools.config.tool({ operation })).toBe(operation) + expect(SSHV2Block.tools.config?.tool({ operation })).toBe(currentId) + expect(SSHV2Block.tools.access).toContain(currentId) + }) + + it('preserves the command default and explicit read-content operation', () => { + expect(SSHV2Block.tools.config?.tool({})).toBe('ssh_execute_command') + expect(SSHV2Block.outputs).not.toHaveProperty('fileContent') + expect(SSHV2Block.outputs.file.type).toBe('file') + expect(SSHV2Block.outputs.content.condition).toEqual({ + field: 'operation', + value: 'ssh_read_file_content', + }) + for (const key of ['success', 'message']) { + expect(SSHV2Block.outputs[key].condition).toEqual({ + field: 'operation', + value: 'ssh_download_file', + not: true, + }) + } + }) +}) diff --git a/apps/sim/blocks/blocks/ssh.ts b/apps/sim/blocks/blocks/ssh.ts index a991a0ea6c8..c49a3a4de0c 100644 --- a/apps/sim/blocks/blocks/ssh.ts +++ b/apps/sim/blocks/blocks/ssh.ts @@ -1,12 +1,16 @@ import { ClipboardList, Download, File, Search, Server, Wrench } from '@sim/emcn/icons' +import { omit } from '@sim/utils/object' import { SshIcon, SshTerminalIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { createVersionedToolSelector } from '@/blocks/utils' import type { SSHResponse } from '@/tools/ssh/types' -export const SSHBlock: BlockConfig = { +export const SSHBlock = { type: 'ssh', - name: 'SSH', + name: 'SSH (Legacy)', + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'ssh_v2' }, description: 'Connect to remote servers via SSH', authMode: AuthMode.ApiKey, longDescription: @@ -661,6 +665,48 @@ Examples: os: { type: 'string', description: 'Operating system' }, message: { type: 'string', description: 'Operation status message' }, }, +} satisfies BlockConfig + +const selectSshV2Tool = createVersionedToolSelector({ + baseToolSelector: SSHBlock.tools.config.tool, + suffix: '_v2', + fallbackToolId: 'ssh_download_file_v2', +}) + +export const SSHV2Block: BlockConfig = { + ...SSHBlock, + type: 'ssh_v2', + name: 'SSH', + hideFromToolbar: false, + sunset: undefined, + tools: { + ...SSHBlock.tools, + access: SSHBlock.tools.access.map((toolId) => + toolId === 'ssh_download_file' ? 'ssh_download_file_v2' : toolId + ), + config: { + ...SSHBlock.tools.config, + tool: (params) => + params.operation === 'ssh_download_file' + ? selectSshV2Tool(params) + : SSHBlock.tools.config.tool(params), + }, + }, + outputs: { + ...omit(SSHBlock.outputs, ['fileContent']), + success: { + ...SSHBlock.outputs.success, + condition: { field: 'operation', value: 'ssh_download_file', not: true }, + }, + message: { + ...SSHBlock.outputs.message, + condition: { field: 'operation', value: 'ssh_download_file', not: true }, + }, + content: { + ...SSHBlock.outputs.content, + condition: { field: 'operation', value: 'ssh_read_file_content' }, + }, + }, } export const SSHBlockMeta = { diff --git a/apps/sim/blocks/registry-maps.ts b/apps/sim/blocks/registry-maps.ts index 672ebb5e0e1..b75dc96d948 100644 --- a/apps/sim/blocks/registry-maps.ts +++ b/apps/sim/blocks/registry-maps.ts @@ -25,7 +25,7 @@ import { } 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 { BoxBlock, BoxBlockMeta, BoxV2Block } from '@/blocks/blocks/box' import { BrandfetchBlock, BrandfetchBlockMeta } from '@/blocks/blocks/brandfetch' import { BrexBlock, BrexBlockMeta } from '@/blocks/blocks/brex' import { BrightDataBlock, BrightDataBlockMeta } from '@/blocks/blocks/brightdata' @@ -64,10 +64,10 @@ import { DevinBlock, DevinBlockMeta } from '@/blocks/blocks/devin' import { DiscordBlock, DiscordBlockMeta } from '@/blocks/blocks/discord' import { DocuSignBlock, DocuSignBlockMeta } from '@/blocks/blocks/docusign' import { DowndetectorBlock, DowndetectorBlockMeta } from '@/blocks/blocks/downdetector' -import { DropboxBlock, DropboxBlockMeta } from '@/blocks/blocks/dropbox' +import { DropboxBlock, DropboxBlockMeta, DropboxV2Block } from '@/blocks/blocks/dropbox' import { DropcontactBlock, DropcontactBlockMeta } from '@/blocks/blocks/dropcontact' import { DSPyBlock, DSPyBlockMeta } from '@/blocks/blocks/dspy' -import { DubBlock, DubBlockMeta } from '@/blocks/blocks/dub' +import { DubBlock, DubBlockMeta, DubV2Block } from '@/blocks/blocks/dub' import { DuckDuckGoBlock, DuckDuckGoBlockMeta } from '@/blocks/blocks/duckduckgo' import { DynamoDBBlock, DynamoDBBlockMeta } from '@/blocks/blocks/dynamodb' import { DynatraceBlock, DynatraceBlockMeta } from '@/blocks/blocks/dynatrace' @@ -173,7 +173,7 @@ import { JiraServiceManagementBlockMeta, } from '@/blocks/blocks/jira_service_management' import { JotformBlock, JotformBlockMeta } from '@/blocks/blocks/jotform' -import { JupyterBlock, JupyterBlockMeta } from '@/blocks/blocks/jupyter' +import { JupyterBlock, JupyterBlockMeta, JupyterV2Block } from '@/blocks/blocks/jupyter' import { KalshiBlock, KalshiBlockMeta, @@ -209,6 +209,7 @@ import { MicrosoftAdBlock, MicrosoftAdBlockMeta } from '@/blocks/blocks/microsof import { MicrosoftDataverseBlock, MicrosoftDataverseBlockMeta, + MicrosoftDataverseV2Block, } from '@/blocks/blocks/microsoft_dataverse' import { MicrosoftDynamics365Block, @@ -272,7 +273,7 @@ import { PulseBlock, PulseBlockMeta, PulseV2Block } from '@/blocks/blocks/pulse' import { QdrantBlock, QdrantBlockMeta } from '@/blocks/blocks/qdrant' import { QuartrBlock, QuartrBlockMeta } from '@/blocks/blocks/quartr' import { QuickBooksBlock, QuickBooksBlockMeta } from '@/blocks/blocks/quickbooks' -import { QuiverBlock, QuiverBlockMeta } from '@/blocks/blocks/quiver' +import { QuiverBlock, QuiverBlockMeta, QuiverV2Block } from '@/blocks/blocks/quiver' import { RabbitmqBlock, RabbitmqBlockMeta } from '@/blocks/blocks/rabbitmq' import { RailwayBlock, RailwayBlockMeta } from '@/blocks/blocks/railway' import { RB2BBlock, RB2BBlockMeta } from '@/blocks/blocks/rb2b' @@ -301,9 +302,9 @@ import { SendblueBlock, SendblueBlockMeta } from '@/blocks/blocks/sendblue' import { SendGridBlock, SendGridBlockMeta } from '@/blocks/blocks/sendgrid' import { SentryBlock, SentryBlockMeta } from '@/blocks/blocks/sentry' import { SerperBlock, SerperBlockMeta } from '@/blocks/blocks/serper' -import { ServiceNowBlock, ServiceNowBlockMeta } from '@/blocks/blocks/servicenow' +import { ServiceNowBlock, ServiceNowBlockMeta, ServiceNowV2Block } from '@/blocks/blocks/servicenow' import { SESBlock, SESBlockMeta } from '@/blocks/blocks/ses' -import { SftpBlock, SftpBlockMeta } from '@/blocks/blocks/sftp' +import { SftpBlock, SftpBlockMeta, SftpV2Block } from '@/blocks/blocks/sftp' import { SharepointBlock, SharepointBlockMeta, SharepointV2Block } from '@/blocks/blocks/sharepoint' import { ShopifyBlock, ShopifyBlockMeta } from '@/blocks/blocks/shopify' import { SimWorkspaceEventBlock } from '@/blocks/blocks/sim_workspace_event' @@ -318,7 +319,7 @@ import { SportmonksBlock, SportmonksBlockMeta } from '@/blocks/blocks/sportmonks import { SpotifyBlock, SpotifyBlockMeta } from '@/blocks/blocks/spotify' import { SQSBlock, SQSBlockMeta } from '@/blocks/blocks/sqs' import { SquareBlock, SquareBlockMeta } from '@/blocks/blocks/square' -import { SSHBlock, SSHBlockMeta } from '@/blocks/blocks/ssh' +import { SSHBlock, SSHBlockMeta, SSHV2Block } from '@/blocks/blocks/ssh' import { SSMBlock, SSMBlockMeta } from '@/blocks/blocks/ssm' import { StagehandBlock, StagehandBlockMeta } from '@/blocks/blocks/stagehand' import { StartTriggerBlock } from '@/blocks/blocks/start_trigger' @@ -406,6 +407,7 @@ export const BLOCK_REGISTRY: Record = { azure_devops: AzureDevOpsBlock, bitbucket: BitbucketBlock, box: BoxBlock, + box_v2: BoxV2Block, brandfetch: BrandfetchBlock, brex: BrexBlock, brightdata: BrightDataBlock, @@ -447,9 +449,11 @@ export const BLOCK_REGISTRY: Record = { docusign: DocuSignBlock, downdetector: DowndetectorBlock, dropbox: DropboxBlock, + dropbox_v2: DropboxV2Block, dropcontact: DropcontactBlock, dspy: DSPyBlock, dub: DubBlock, + dub_v2: DubV2Block, duckduckgo: DuckDuckGoBlock, dynamodb: DynamoDBBlock, dynatrace: DynatraceBlock, @@ -541,6 +545,7 @@ export const BLOCK_REGISTRY: Record = { jira_service_management: JiraServiceManagementBlock, jotform: JotformBlock, jupyter: JupyterBlock, + jupyter_v2: JupyterV2Block, kalshi: KalshiBlock, kalshi_v2: KalshiV2Block, ketch: KetchBlock, @@ -572,6 +577,7 @@ export const BLOCK_REGISTRY: Record = { memory: MemoryBlock, microsoft_ad: MicrosoftAdBlock, microsoft_dataverse: MicrosoftDataverseBlock, + microsoft_dataverse_v2: MicrosoftDataverseV2Block, microsoft_dynamics_365: MicrosoftDynamics365Block, microsoft_excel: MicrosoftExcelBlock, microsoft_excel_v2: MicrosoftExcelV2Block, @@ -620,6 +626,7 @@ export const BLOCK_REGISTRY: Record = { quartr: QuartrBlock, quickbooks: QuickBooksBlock, quiver: QuiverBlock, + quiver_v2: QuiverV2Block, rabbitmq: RabbitmqBlock, railway: RailwayBlock, rb2b: RB2BBlock, @@ -651,8 +658,10 @@ export const BLOCK_REGISTRY: Record = { sentry: SentryBlock, serper: SerperBlock, servicenow: ServiceNowBlock, + servicenow_v2: ServiceNowV2Block, ses: SESBlock, sftp: SftpBlock, + sftp_v2: SftpV2Block, sharepoint: SharepointBlock, sharepoint_v2: SharepointV2Block, shopify: ShopifyBlock, @@ -670,6 +679,7 @@ export const BLOCK_REGISTRY: Record = { sqs: SQSBlock, square: SquareBlock, ssh: SSHBlock, + ssh_v2: SSHV2Block, ssm: SSMBlock, stagehand: StagehandBlock, start_trigger: StartTriggerBlock, @@ -875,6 +885,7 @@ export const BLOCK_META_REGISTRY: Record = { jira_service_management: JiraServiceManagementBlockMeta, jotform: JotformBlockMeta, jupyter: JupyterBlockMeta, + jupyter_v2: JupyterBlockMeta, kalshi: KalshiBlockMeta, kalshi_v2: KalshiV2BlockMeta, ketch: KetchBlockMeta, @@ -970,6 +981,7 @@ export const BLOCK_META_REGISTRY: Record = { servicenow: ServiceNowBlockMeta, ses: SESBlockMeta, sftp: SftpBlockMeta, + sftp_v2: SftpBlockMeta, sharepoint: SharepointBlockMeta, shopify: ShopifyBlockMeta, similarweb: SimilarwebBlockMeta, @@ -985,6 +997,7 @@ export const BLOCK_META_REGISTRY: Record = { sqs: SQSBlockMeta, square: SquareBlockMeta, ssh: SSHBlockMeta, + ssh_v2: SSHBlockMeta, ssm: SSMBlockMeta, stagehand: StagehandBlockMeta, stripe: StripeBlockMeta, diff --git a/apps/sim/executor/utils/file-tool-processor.aliases.test.ts b/apps/sim/executor/utils/file-tool-processor.aliases.test.ts new file mode 100644 index 00000000000..040c338e00c --- /dev/null +++ b/apps/sim/executor/utils/file-tool-processor.aliases.test.ts @@ -0,0 +1,107 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import type { UserFile } from '@/executor/types' +import type { ToolDefinition } from '@/tools/types' + +vi.mock('@/lib/internal/tool-operations/file-result.server', () => ({ + storeInternalToolFileResult: vi.fn(), +})) +vi.mock('@/lib/uploads/utils/file-utils.server', () => ({ downloadFileFromUrl: vi.fn() })) + +import { FileToolProcessor } from '@/executor/utils/file-tool-processor' + +const STORED_FILE: UserFile = { + id: 'file-1', + key: 'execution/file-1', + url: '/api/files/serve/execution/file-1', + name: 'notes.txt', + type: 'text/plain', + size: 5, + base64: 'aGVsbG8=', +} +const TOOL: ToolDefinition = { + id: 'alias-test', + name: 'Alias Test', + description: 'File aliases', + version: '1.0.0', + params: {}, + outputs: { file: { type: 'file', description: 'Stored file' } }, +} +const CONTEXT = { workflowId: 'workflow-1', executionId: 'execution-1', workspaceId: 'workspace-1' } + +describe('file output alias replacement', () => { + it('handles deeply nested, small JSON metadata without recursive stack growth', async () => { + const depth = 20_000 + const json = `${'{"child":'.repeat(depth)}null${'}'.repeat(depth)}` + const metadata: Record = JSON.parse(json) + const result = await FileToolProcessor.processToolOutputs( + { file: STORED_FILE, metadata }, + TOOL, + CONTEXT + ) + + expect(json.length).toBeLessThan(1024 * 1024) + expect(result.file).not.toHaveProperty('base64') + expect(result.metadata === metadata).toBe(false) + let cursor: unknown = result.metadata + let actualDepth = 0 + while (cursor && typeof cursor === 'object' && 'child' in cursor) { + actualDepth++ + cursor = cursor.child + } + expect(actualDepth).toBe(depth) + expect(cursor).toBeNull() + }) + + it('preserves cycles and shared aliases while replacing every reference to the file', async () => { + const shared = { file: STORED_FILE } + const input: Record = { + file: STORED_FILE, + messages: [shared, shared], + } + input.self = input + const result = await FileToolProcessor.processToolOutputs(input, TOOL, CONTEXT) + expect(result.self).toBe(result) + const messages = result.messages as Array<{ file: UserFile }> + expect(messages[0]).toBe(messages[1]) + expect(messages[0]?.file).toBe(result.file) + expect(result.file).not.toHaveProperty('base64') + expect(STORED_FILE.base64).toBe('aGVsbG8=') + expect(input.self).toBe(input) + }) + + it('keeps buffers, stored files, and non-plain objects as leaves', async () => { + const buffer = Buffer.alloc(12 * 1024 * 1024) + const date = new Date('2026-01-01') + const existing = { ...STORED_FILE, id: 'existing-file', base64: undefined } + const result = await FileToolProcessor.processToolOutputs( + { file: STORED_FILE, metadata: { buffer, date, existing } }, + TOOL, + CONTEXT + ) + const metadata = result.metadata as Record + expect(metadata.buffer).toBe(buffer) + expect(metadata.date).toBe(date) + expect(metadata.existing).toBe(existing) + }) + + it('preserves null prototypes and own __proto__ keys without modifying prototypes', async () => { + const metadata: Record = Object.create(null) + metadata.file = STORED_FILE + const keys = JSON.parse('{"__proto__":{"file":null}}') + keys.__proto__.file = STORED_FILE + const result = await FileToolProcessor.processToolOutputs( + { file: STORED_FILE, metadata, keys }, + TOOL, + CONTEXT + ) + expect(Object.getPrototypeOf(result.metadata)).toBeNull() + const copiedKeys = result.keys as Record + expect(Object.getPrototypeOf(copiedKeys)).toBe(Object.prototype) + expect(Object.hasOwn(copiedKeys, '__proto__')).toBe(true) + expect((copiedKeys.__proto__ as Record).file).toBe(result.file) + expect({}).not.toHaveProperty('file') + }) +}) diff --git a/apps/sim/executor/utils/file-tool-processor.context.test.ts b/apps/sim/executor/utils/file-tool-processor.context.test.ts new file mode 100644 index 00000000000..84d36d284a9 --- /dev/null +++ b/apps/sim/executor/utils/file-tool-processor.context.test.ts @@ -0,0 +1,167 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { InternalToolOperationContext } from '@/lib/internal/tool-operations/types' +import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' +import type { UserFile } from '@/executor/types' +import type { ToolConfig } from '@/tools/types' + +const mocks = vi.hoisted(() => ({ + download: vi.fn(), + uploadExecution: vi.fn(), + uploadCopilot: vi.fn(), + deleteFile: vi.fn(), + deleteMetadata: vi.fn(), +})) + +vi.mock('@/lib/uploads/utils/file-utils.server', () => ({ downloadFileFromUrl: mocks.download })) +vi.mock('@/lib/uploads/contexts/execution', () => ({ uploadExecutionFile: mocks.uploadExecution })) +vi.mock('@/lib/uploads/contexts/copilot', () => ({ uploadCopilotFile: mocks.uploadCopilot })) +vi.mock('@/lib/uploads/core/storage-service', () => ({ deleteFile: mocks.deleteFile })) +vi.mock('@/lib/uploads/server/metadata', () => ({ deleteFileMetadata: mocks.deleteMetadata })) + +import { FileToolProcessor } from '@/executor/utils/file-tool-processor' + +const context: InternalToolOperationContext = { + workflowId: '', + userId: 'actor-1', + workspaceId: 'workspace-1', + copilotToolExecution: true, +} +const tool = { + id: 'test_attachments', + name: 'Test attachments', + description: 'Downloads message attachments', + version: '1.0.0', + params: {}, + request: { url: 'https://example.com/messages', method: 'GET' }, + outputs: { files: { type: 'file[]' } }, +} satisfies ToolConfig + +const stored: UserFile = { + id: 'file-1', + key: 'copilot/actor-1/file-1/workbook.xlsx', + name: 'workbook.xlsx', + type: 'application/octet-stream', + size: 12 * 1024 * 1024, + url: 'https://storage.example/workbook.xlsx', + context: 'copilot', +} + +describe('file output processing across trusted contexts', () => { + beforeEach(() => { + vi.resetAllMocks() + mocks.uploadCopilot.mockResolvedValue(stored) + }) + + it('stores a large late attachment once and replaces every nested alias for Copilot', async () => { + const bytes = Buffer.alloc(12 * 1024 * 1024) + const attachment = { name: 'workbook.xlsx', contentType: stored.type, data: bytes } + const input = { files: [attachment, attachment], results: [{ attachments: [attachment] }] } + + const result = await FileToolProcessor.processToolOutputs(input, tool, context) + + expect(result).toEqual({ files: [stored, stored], results: [{ attachments: [stored] }] }) + expect((result.files as UserFile[])[0]).toBe(stored) + expect(mocks.uploadCopilot).toHaveBeenCalledOnce() + expect(mocks.uploadCopilot.mock.calls[0]?.[0].buffer).toBe(bytes) + expect(mocks.uploadCopilot.mock.calls[0]?.[0].userId).toBe('actor-1') + expect(mocks.uploadExecution).not.toHaveBeenCalled() + expect(JSON.stringify(result).length).toBeLessThan(2048) + expect(input.results[0]?.attachments[0]?.data).toBe(bytes) + }) + + it('rejects an aggregate over budget before uploading any file', async () => { + const first = Buffer.alloc(1) + Object.defineProperty(first, 'length', { value: MAX_FILE_SIZE }) + await expect( + FileToolProcessor.processToolOutputs( + { + files: [ + { name: 'first.bin', data: first }, + { name: 'second.bin', data: Buffer.alloc(1) }, + ], + }, + tool, + context + ) + ).rejects.toThrow('exceeds the maximum allowed size') + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + }) + + it('validates all files before creating storage objects', async () => { + await expect( + FileToolProcessor.processToolOutputs( + { + files: [ + { name: 'valid.txt', data: Buffer.from('valid') }, + { name: 'invalid.txt', data: '?' }, + ], + }, + tool, + context + ) + ).rejects.toThrow('invalid base64') + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + }) + + it('passes the remaining aggregate budget and cancellation signal to URL downloads', async () => { + const controller = new AbortController() + mocks.download.mockImplementation(async () => { + controller.abort(new Error('Download cancelled')) + return Buffer.alloc(1) + }) + await expect( + FileToolProcessor.processToolOutputs( + { + files: [ + { name: 'first.txt', data: Buffer.alloc(3) }, + { name: 'second.txt', url: 'https://example.com/file' }, + ], + }, + tool, + context, + controller.signal + ) + ).rejects.toThrow('Download cancelled') + expect(mocks.download).toHaveBeenCalledWith('https://example.com/file', { + userId: 'actor-1', + maxBytes: MAX_FILE_SIZE - 3, + signal: controller.signal, + }) + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + }) + + it('removes materialized base64 from existing references and their nested aliases', async () => { + const materialized = { ...stored, base64: 'c2VjcmV0' } + const result = await FileToolProcessor.processToolOutputs( + { files: [materialized], messages: [{ file: materialized }] }, + tool, + context + ) + expect(result).toEqual({ files: [stored], messages: [{ file: stored }] }) + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + expect(mocks.download).not.toHaveBeenCalled() + }) + + it('rolls back an unpublished attachment if a later upload fails', async () => { + mocks.uploadCopilot + .mockResolvedValueOnce(stored) + .mockRejectedValueOnce(new Error('Storage down')) + await expect( + FileToolProcessor.processToolOutputs( + { + files: [ + { name: 'first.txt', data: Buffer.from('a') }, + { name: 'second.txt', data: Buffer.from('b') }, + ], + }, + tool, + context + ) + ).rejects.toThrow('Storage down') + expect(mocks.deleteFile).toHaveBeenCalledWith({ key: stored.key, context: 'copilot' }) + expect(mocks.deleteMetadata).toHaveBeenCalledWith(stored.key) + }) +}) diff --git a/apps/sim/executor/utils/file-tool-processor.test.ts b/apps/sim/executor/utils/file-tool-processor.test.ts index 549dbedf2a2..a32cee604bf 100644 --- a/apps/sim/executor/utils/file-tool-processor.test.ts +++ b/apps/sim/executor/utils/file-tool-processor.test.ts @@ -57,6 +57,28 @@ describe('FileToolProcessor', () => { } satisfies UserFile) }) + it('passes stored file descriptors through without downloading or uploading again', async () => { + const stored: UserFile = { + id: 'file-1', + key: 'execution/workspace-1/workflow-1/execution-1/file-1/workbook.xlsx', + name: 'workbook.xlsx', + size: 12 * 1024 * 1024, + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + url: 'https://storage.example/workbook.xlsx', + context: 'execution', + } + + const result = await FileToolProcessor.processToolOutputs( + { file: stored }, + toolConfig, + executionContext + ) + + expect(result.file).toBe(stored) + expect(mockUploadExecutionFile).not.toHaveBeenCalled() + expect(mockDownloadFileFromUrl).not.toHaveBeenCalled() + }) + it('caps URL downloads and stores raster images using byte-derived metadata', async () => { const png = Buffer.concat([ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), diff --git a/apps/sim/executor/utils/file-tool-processor.ts b/apps/sim/executor/utils/file-tool-processor.ts index d66dea14763..e81a2134539 100644 --- a/apps/sim/executor/utils/file-tool-processor.ts +++ b/apps/sim/executor/utils/file-tool-processor.ts @@ -1,265 +1,219 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { omit } from '@sim/utils/object' import { isCanonicalBase64 } from '@/lib/api/contracts/primitives' -import { isUserFile } from '@/lib/core/utils/user-file' -import { uploadExecutionFile, uploadFileFromRawData } from '@/lib/uploads/contexts/execution' +import { isUserFile, type UserFileLike } from '@/lib/core/utils/user-file' +import { + createInternalToolFilesResult, + type InternalToolFile, +} from '@/lib/internal/tool-operations/file-result' +import { storeInternalToolFileResult } from '@/lib/internal/tool-operations/file-result.server' +import type { InternalToolOperationContext } from '@/lib/internal/tool-operations/types' import { downloadFileFromUrl } from '@/lib/uploads/utils/file-utils.server' -import { MAX_FILE_SIZE, sniffImageContentType } from '@/lib/uploads/utils/validation' -import type { ExecutionContext, UserFile } from '@/executor/types' -import type { ToolDefinition, ToolFileData } from '@/tools/types' +import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' +import type { UserFile } from '@/executor/types' +import type { ToolDefinition } from '@/tools/types' const logger = createLogger('FileToolProcessor') -const IMAGE_FILE_EXTENSIONS: Record = { - 'image/gif': 'gif', - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'image/webp': 'webp', +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) } -/** - * Strip a base64 `data:` URI prefix, leaving the encoded payload. An empty payload is - * a legitimate zero-byte file; a payload that only looks empty after normalization is - * not, so callers compare against what this returns rather than the raw value. - */ +/** Strip a data URI prefix while preserving legitimate zero-byte payloads. */ function stripBase64DataUri(value: string): string { return /^data:[^,]*;base64,/i.test(value) ? value.slice(value.indexOf(',') + 1) : value } -/** - * Normalize a base64 payload to canonical RFC 4648 form so it can be validated: drop - * the line wrapping MIME encoders emit, translate the base64url alphabet, and restore - * the padding unpadded encoders omit. - */ +/** Normalize wrapped or unpadded base64url into canonical RFC 4648 form. */ function normalizeBase64(payload: string): string { const compact = payload.replace(/\s/g, '').replace(/-/g, '+').replace(/_/g, '/') const remainder = compact.length % 4 return remainder === 0 ? compact : compact + '='.repeat(4 - remainder) } -function assertFileSize(size: number, fileName: string): void { - if (size > MAX_FILE_SIZE) { - throw new Error(`File '${fileName}' exceeds the maximum allowed size of ${MAX_FILE_SIZE} bytes`) +function assertFileSize(size: number, name: string, remainingBytes: number): void { + if (size > remainingBytes) { + throw new Error(`File '${name}' exceeds the maximum allowed size of ${remainingBytes} bytes`) } } -function resolveStoredFileMetadata( - fileName: string, - declaredMimeType: string, - buffer: Buffer -): { fileName: string; mimeType: string } { - if (!declaredMimeType.startsWith('image/')) { - return { fileName, mimeType: declaredMimeType } - } - - const mimeType = sniffImageContentType(buffer) - if (!mimeType) { - return { - fileName: `${fileName.replace(/\.[^.]+$/, '')}.bin`, - mimeType: 'application/octet-stream', +/** Replaces aliases by identity, preserving message-to-file associations without inline bytes. */ +function replaceFileReferences( + value: unknown, + replacements: ReadonlyMap, + visited = new WeakMap() +): unknown { + type PendingCopy = + | { kind: 'array'; source: unknown[]; target: unknown[] } + | { kind: 'object'; source: object; target: Record } + const pending: PendingCopy[] = [] + + function copyOrReplace(item: unknown): unknown { + if (typeof item !== 'object' || item === null) return item + const replacement = replacements.get(item) + if (replacement) return replacement + if (isUserFile(item) || Buffer.isBuffer(item)) return item + if (visited.has(item)) return visited.get(item) + if (Array.isArray(item)) { + const target: unknown[] = [] + visited.set(item, target) + pending.push({ kind: 'array', source: item, target }) + return target } + const prototype = Object.getPrototypeOf(item) + if (prototype !== Object.prototype && prototype !== null) return item + const target: Record = Object.create(prototype) + visited.set(item, target) + pending.push({ kind: 'object', source: item, target }) + return target } - const extension = IMAGE_FILE_EXTENSIONS[mimeType] - return { - fileName: extension ? `${fileName.replace(/\.[^.]+$/, '')}.${extension}` : fileName, - mimeType, + const result = copyOrReplace(value) + while (pending.length > 0) { + const copy = pending.pop()! + if (copy.kind === 'array') { + for (const item of copy.source) copy.target.push(copyOrReplace(item)) + } else { + for (const [key, item] of Object.entries(copy.source)) { + Object.defineProperty(copy.target, key, { + value: copyOrReplace(item), + enumerable: true, + writable: true, + configurable: true, + }) + } + } } + return result } -/** - * Processes tool outputs and converts file-typed outputs to UserFile objects. - * This enables tools to return file data that gets automatically stored in the - * execution filesystem and made available as UserFile objects for workflow use. - */ +/** Stores declared file outputs once, for both workflow and Copilot callers. */ export class FileToolProcessor { - /** - * Process tool outputs and convert file-typed outputs to UserFile objects - */ static async processToolOutputs( - toolOutput: any, + toolOutput: Record, toolConfig: ToolDefinition, - executionContext: ExecutionContext - ): Promise { - if (!toolConfig.outputs) { - return toolOutput - } - - const processedOutput = { ...toolOutput } + context: InternalToolOperationContext, + signal?: AbortSignal + ): Promise> { + if (!toolConfig.outputs) return toolOutput + signal?.throwIfAborted() + const pendingFiles = new Map() + const replacements = new Map() + let remainingBytes = MAX_FILE_SIZE for (const [outputKey, outputDef] of Object.entries(toolConfig.outputs)) { - if (!FileToolProcessor.isFileOutput(outputDef.type)) { - continue - } - - const fileData = processedOutput[outputKey] - if (!fileData) { - logger.warn(`File-typed output '${outputKey}' is missing from tool result`) - continue - } - + if (outputDef.type !== 'file' && outputDef.type !== 'file[]') continue + const value = toolOutput[outputKey] + if (value === undefined || value === null) continue try { - processedOutput[outputKey] = await FileToolProcessor.processFileOutput( - fileData, - outputDef.type, - outputKey, - executionContext - ) + if (outputDef.type === 'file[]' && !Array.isArray(value)) { + throw new Error(`Output '${outputKey}' is marked as file[] but is not an array`) + } + const files = outputDef.type === 'file[]' && Array.isArray(value) ? value : [value] + for (const file of files) { + signal?.throwIfAborted() + if (!isRecord(file)) throw new Error('File output must be a file object') + if (isUserFile(file)) { + if (file.base64 !== undefined) replacements.set(file, omit(file, ['base64'])) + continue + } + if (pendingFiles.has(file)) continue + const buffered = await FileToolProcessor.readFile(file, context, remainingBytes, signal) + remainingBytes -= buffered.buffer.length + pendingFiles.set(file, buffered) + } } catch (error) { + signal?.throwIfAborted() logger.error(`Error processing file output '${outputKey}':`, error) - const errorMessage = toError(error).message - throw new Error(`Failed to process file output '${outputKey}': ${errorMessage}`) + throw new Error(`Failed to process file output '${outputKey}': ${toError(error).message}`) } } - return processedOutput - } - - /** - * Check if an output type is file-related - */ - private static isFileOutput(type: string): boolean { - return type === 'file' || type === 'file[]' - } - - /** - * Process a single file output (either single file or array of files) - */ - private static async processFileOutput( - fileData: any, - outputType: string, - outputKey: string, - executionContext: ExecutionContext - ): Promise { - if (outputType === 'file[]') { - return FileToolProcessor.processFileArray(fileData, outputKey, executionContext) - } - return FileToolProcessor.processFileData(fileData, executionContext) - } - - /** - * Process an array of files - */ - private static async processFileArray( - fileData: any, - outputKey: string, - executionContext: ExecutionContext - ): Promise { - if (!Array.isArray(fileData)) { - throw new Error(`Output '${outputKey}' is marked as file[] but is not an array`) + const originals = [...pendingFiles.keys()] + const present = (files: readonly UserFile[]) => { + originals.forEach((original, index) => { + replacements.set(original, files[index]!) + }) + if (replacements.size === 0) return toolOutput + const output = replaceFileReferences(toolOutput, replacements) + if (!isRecord(output)) throw new Error('Tool file output must be an object') + return output } - - const files: UserFile[] = [] - for (const file of fileData) { - files.push(await FileToolProcessor.processFileData(file, executionContext)) - } - return files + if (pendingFiles.size === 0) return present([]) + return storeInternalToolFileResult( + createInternalToolFilesResult([...pendingFiles.values()], present), + context, + (output) => { + if (!isRecord(output)) throw new Error('Tool file output must be an object') + return output + }, + signal + ) } - /** - * Convert various file data formats to UserFile by storing in execution filesystem. - * If the input is already a UserFile, returns it unchanged. - */ - private static async processFileData( - fileData: ToolFileData | UserFile, - context: ExecutionContext - ): Promise { - // If already a UserFile (e.g., from tools that handle their own file storage), - // return it directly without re-processing - if (isUserFile(fileData)) { - return fileData as UserFile + private static async readFile( + file: Record, + context: InternalToolOperationContext, + remainingBytes: number, + signal?: AbortSignal + ): Promise { + if (typeof file.name !== 'string' || !file.name.trim()) { + throw new Error('File output requires a filename') } - - const data = fileData as ToolFileData - try { - let buffer: Buffer | null = null - - if (Buffer.isBuffer(data.data)) { - assertFileSize(data.data.length, data.name) - buffer = data.data - } else if ( - data.data && - typeof data.data === 'object' && - 'type' in data.data && - 'data' in data.data - ) { - const serializedBuffer = data.data as { type: string; data: number[] } - if (serializedBuffer.type === 'Buffer' && Array.isArray(serializedBuffer.data)) { - assertFileSize(serializedBuffer.data.length, data.name) - buffer = Buffer.from(serializedBuffer.data) - } else { - throw new Error(`Invalid serialized buffer format for ${data.name}`) - } - } else if (typeof data.data === 'string') { - const payload = stripBase64DataUri(data.data) - const base64Data = normalizeBase64(payload) - - const paddingBytes = base64Data.endsWith('==') ? 2 : base64Data.endsWith('=') ? 1 : 0 - assertFileSize(Math.floor((base64Data.length * 3) / 4) - paddingBytes, data.name) - if (!isCanonicalBase64(base64Data) || (payload.length > 0 && base64Data.length === 0)) { - throw new Error(`File '${data.name}' has invalid base64 data`) - } - buffer = Buffer.from(base64Data, 'base64') + const name = file.name + const mimeType = + (typeof file.mimeType === 'string' && file.mimeType) || + (typeof file.contentType === 'string' && file.contentType) || + 'application/octet-stream' + let buffer: Buffer | undefined + const data = file.data + + if (Buffer.isBuffer(data)) { + assertFileSize(data.length, name, remainingBytes) + buffer = data + } else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { + assertFileSize(data.byteLength, name, remainingBytes) + buffer = + data instanceof ArrayBuffer + ? Buffer.from(data) + : Buffer.from(data.buffer, data.byteOffset, data.byteLength) + } else if (Array.isArray(data) || (isRecord(data) && data.type === 'Buffer')) { + const bytes = Array.isArray(data) ? data : data.data + if (!Array.isArray(bytes)) throw new Error(`Invalid serialized buffer format for ${name}`) + assertFileSize(bytes.length, name, remainingBytes) + if (!bytes.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error(`Invalid serialized buffer format for ${name}`) } - - if ((!buffer || buffer.length === 0) && data.url) { - buffer = await downloadFileFromUrl(data.url, { - maxBytes: MAX_FILE_SIZE, - userId: context.userId, - }) - } - - if (buffer) { - assertFileSize(buffer.length, data.name) - const storedMetadata = resolveStoredFileMetadata(data.name, data.mimeType, buffer) - - return await uploadExecutionFile( - { - workspaceId: context.workspaceId || '', - workflowId: context.workflowId, - executionId: context.executionId || '', - }, - buffer, - storedMetadata.fileName, - storedMetadata.mimeType, - context.userId - ) - } - - if (!data.data) { - throw new Error( - `File data for '${data.name}' must have either 'data' (Buffer/base64) or 'url' property` - ) + buffer = Buffer.from(bytes) + } else if (typeof data === 'string') { + const payload = stripBase64DataUri(data) + const base64 = normalizeBase64(payload) + const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0 + assertFileSize(Math.floor((base64.length * 3) / 4) - padding, name, remainingBytes) + if (!isCanonicalBase64(base64) || (payload.length > 0 && base64.length === 0)) { + throw new Error(`File '${name}' has invalid base64 data`) } + buffer = Buffer.from(base64, 'base64') + } - return uploadFileFromRawData( - { - name: data.name, - data: data.data, - mimeType: data.mimeType, - }, - { - workspaceId: context.workspaceId || '', - workflowId: context.workflowId, - executionId: context.executionId || '', - }, - context.userId - ) - } catch (error) { - logger.error(`Error processing file data for '${data.name}':`, error) - throw error + if ((!buffer || buffer.length === 0) && typeof file.url === 'string' && file.url) { + buffer = await downloadFileFromUrl(file.url, { + maxBytes: remainingBytes, + userId: context.userId, + ...(signal ? { signal } : {}), + }) + } + signal?.throwIfAborted() + if (!buffer) { + throw new Error(`File data for '${name}' must have either 'data' (Buffer/base64) or 'url'`) } + assertFileSize(buffer.length, name, remainingBytes) + return { buffer, name, mimeType } } - /** - * Check if a tool has any file-typed outputs - */ static hasFileOutputs(toolConfig: ToolDefinition): boolean { - if (!toolConfig.outputs) { - return false - } - - return Object.values(toolConfig.outputs).some( + return Object.values(toolConfig.outputs ?? {}).some( (output) => output.type === 'file' || output.type === 'file[]' ) } diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index a9318bcd494..bad2b73663e 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -301,7 +301,7 @@ export const blockTypeToIconMap: Record = { azure_data_explorer: AzureDataExplorerIcon, azure_devops: AzureIcon, bitbucket: BitbucketIcon, - box: BoxCompanyIcon, + box_v2: BoxCompanyIcon, brandfetch: BrandfetchIcon, brex: BrexIcon, brightdata: BrightDataIcon, @@ -337,10 +337,10 @@ export const blockTypeToIconMap: Record = { discord: DiscordIcon, docusign: DocuSignIcon, downdetector: DowndetectorIcon, - dropbox: DropboxIcon, + dropbox_v2: DropboxIcon, dropcontact: DropcontactIcon, dspy: DsPyIcon, - dub: DubIcon, + dub_v2: DubIcon, duckduckgo: DuckDuckGoIcon, dynamodb: DynamoDBIcon, dynatrace: DynatraceIcon, @@ -416,7 +416,7 @@ export const blockTypeToIconMap: Record = { jira_service_management: JiraServiceManagementIcon, jotform: JotformIcon, jsm: JiraServiceManagementIcon, - jupyter: JupyterIcon, + jupyter_v2: JupyterIcon, kalshi_v2: KalshiIcon, ketch: KetchIcon, knowledge: PackageSearchIcon, @@ -443,7 +443,7 @@ export const blockTypeToIconMap: Record = { mem0: Mem0Icon, memory: BrainIcon, microsoft_ad: AzureIcon, - microsoft_dataverse: MicrosoftDataverseIcon, + microsoft_dataverse_v2: MicrosoftDataverseIcon, microsoft_dynamics_365: MicrosoftDataverseIcon, microsoft_excel_v2: MicrosoftExcelIcon, microsoft_planner: MicrosoftPlannerIcon, @@ -486,7 +486,7 @@ export const blockTypeToIconMap: Record = { qdrant: QdrantIcon, quartr: QuartrIcon, quickbooks: QuickBooksIcon, - quiver: QuiverIcon, + quiver_v2: QuiverIcon, rabbitmq: RabbitmqIcon, railway: RailwayIcon, rb2b: RB2BIcon, @@ -512,8 +512,9 @@ export const blockTypeToIconMap: Record = { sentry: SentryIcon, serper: SerperIcon, servicenow: ServiceNowIcon, + servicenow_v2: ServiceNowIcon, ses: SESIcon, - sftp: SftpIcon, + sftp_v2: SftpIcon, sharepoint_v2: MicrosoftSharepointIcon, shopify: ShopifyIcon, sim_workspace_event: SimTriggerIcon, @@ -529,7 +530,7 @@ export const blockTypeToIconMap: Record = { sportmonks: SportmonksIcon, sqs: SQSIcon, square: SquareIcon, - ssh: SshIcon, + ssh_v2: SshIcon, ssm: SSMIcon, stagehand: StagehandIcon, stripe: StripeIcon, diff --git a/apps/sim/lib/internal/agiloft/execute-tool.test.ts b/apps/sim/lib/internal/agiloft/execute-tool.test.ts index d0c98cc55f8..79df94a6178 100644 --- a/apps/sim/lib/internal/agiloft/execute-tool.test.ts +++ b/apps/sim/lib/internal/agiloft/execute-tool.test.ts @@ -3,6 +3,7 @@ */ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const operationMocks = vi.hoisted(() => ({ executeAgiloftAsyncStatus: vi.fn(), @@ -28,7 +29,7 @@ const operationMocks = vi.hoisted(() => ({ vi.mock('@/lib/internal/agiloft/operations', () => operationMocks) import { AgiloftOperationError } from '@/lib/internal/agiloft/errors' -import { executeAgiloftTool } from '@/lib/internal/agiloft/execute-tool' +import { executeAgiloftTool as executeAgiloftToolOperation } from '@/lib/internal/agiloft/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' const CREDENTIALS = { @@ -145,6 +146,14 @@ const TOOL_CASES = [ ], ] as const +async function executeAgiloftTool( + request: Parameters[0] +): Promise { + const result = await executeAgiloftToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeAgiloftTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -164,6 +173,22 @@ describe('executeAgiloftTool', () => { ) }) + it('forwards file bytes without serializing the file result', async () => { + const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ success: true, output: { file } }) + ) + operationMocks.executeAgiloftRetrieveAttachment.mockResolvedValueOnce(fileResult) + expect( + await executeAgiloftToolOperation( + createRequest({ + toolId: 'agiloft_retrieve_attachment', + input: { ...BASE, recordId: '1', fieldName: 'files', position: '0' }, + }) + ) + ).toBe(fileResult) + }) + it('uses the trusted delegation origin and forwards cancellation', async () => { const controller = new AbortController() const input = { ...BASE, data: '{"name":"Contract"}' } diff --git a/apps/sim/lib/internal/agiloft/execute-tool.ts b/apps/sim/lib/internal/agiloft/execute-tool.ts index e59c8ba22ba..5d5377815cd 100644 --- a/apps/sim/lib/internal/agiloft/execute-tool.ts +++ b/apps/sim/lib/internal/agiloft/execute-tool.ts @@ -42,9 +42,11 @@ import { executeAgiloftUpdateRecord, executeAgiloftUpsertRecord, } from '@/lib/internal/agiloft/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import type { InternalToolOperationCall, InternalToolOperationHandler, + InternalToolOperationResult, } from '@/lib/internal/tool-operations/types' function parseInput(contract: C, input: unknown) { @@ -69,7 +71,7 @@ async function executeOperation( contract: C, request: InternalToolOperationCall, operation: (input: ContractBody, context: AgiloftOperationContext) => Promise -): Promise { +): Promise { request.signal?.throwIfAborted() const parsed = parseInput(contract, request.input) if (!parsed.success) return parsed.response @@ -80,7 +82,7 @@ async function executeOperation( signal: request.signal, }) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof AgiloftOperationError) { @@ -93,7 +95,9 @@ async function executeOperation( } } -export const executeAgiloftTool: InternalToolOperationHandler = async (request) => { +export const executeAgiloftTool: InternalToolOperationHandler = async ( + request +) => { switch (request.toolId) { case 'agiloft_async_status': return executeOperation(agiloftAsyncStatusContract, request, executeAgiloftAsyncStatus) diff --git a/apps/sim/lib/internal/agiloft/operations.test.ts b/apps/sim/lib/internal/agiloft/operations.test.ts index a9e373f3244..cd5709a1893 100644 --- a/apps/sim/lib/internal/agiloft/operations.test.ts +++ b/apps/sim/lib/internal/agiloft/operations.test.ts @@ -77,6 +77,17 @@ function createResponse( type ResponseTransform = (response: SecureFetchResponse) => Promise +const storedFile = { + id: 'stored-file', + name: 'stored.bin', + size: 5, + type: 'application/octet-stream', + mimeType: 'application/octet-stream', + url: '/api/files/stored', + key: 'execution/workspace/workflow/run/stored.bin', + context: 'execution', +} as const + describe('Agiloft operations', () => { beforeEach(() => { vi.clearAllMocks() @@ -163,17 +174,10 @@ describe('Agiloft operations', () => { { requestId: 'request-1', signal: controller.signal } ) - expect(result).toEqual({ - success: true, - output: { - file: { - name: 'evidence.txt', - mimeType: 'text/plain', - data: Buffer.from('hello').toString('base64'), - size: 5, - }, - }, - }) + expect(result.files).toEqual([ + { name: 'evidence.txt', mimeType: 'text/plain', buffer: Buffer.from('hello') }, + ]) + expect(result.present([storedFile])).toEqual({ success: true, output: { file: storedFile } }) expect(providerMocks.secureFetchWithPinnedIP).toHaveBeenCalledWith( expect.stringContaining('/ewws/EWRetrieve'), '203.0.113.10', diff --git a/apps/sim/lib/internal/agiloft/operations.ts b/apps/sim/lib/internal/agiloft/operations.ts index a346fd60ef8..ba25c691692 100644 --- a/apps/sim/lib/internal/agiloft/operations.ts +++ b/apps/sim/lib/internal/agiloft/operations.ts @@ -65,6 +65,10 @@ import { getLockHttpMethod, parseFieldList, } from '@/lib/internal/agiloft/urls' +import { + createInternalToolFileResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import { resolveEffectiveMimeType } from '@/lib/uploads/utils/file-utils' import type { AgiloftAsyncStatusResponse, @@ -893,7 +897,7 @@ export async function executeAgiloftAttachFile( export async function executeAgiloftRetrieveAttachment( input: AgiloftRetrieveBody, context: AgiloftOperationContext -): Promise { +): Promise { let resolvedIP: string try { resolvedIP = await resolveAgiloftInstance(input.instanceUrl, context.signal) @@ -930,15 +934,8 @@ export async function executeAgiloftRetrieveAttachment( error: `Agiloft error: ${buffer.toString('utf8').slice(0, 300)}`, }) } - return { - success: true, - output: { - file: { - name: fileName, - mimeType: resolveEffectiveMimeType(contentType, fileName), - data: buffer.toString('base64'), - size: buffer.length, - }, - }, - } + return createInternalToolFileResult( + { buffer, name: fileName, mimeType: resolveEffectiveMimeType(contentType, fileName) }, + (file) => ({ success: true, output: { file } }) + ) } diff --git a/apps/sim/lib/internal/cursor/execute-tool.test.ts b/apps/sim/lib/internal/cursor/execute-tool.test.ts index a290d6a7f06..e68a9e6bf1d 100644 --- a/apps/sim/lib/internal/cursor/execute-tool.test.ts +++ b/apps/sim/lib/internal/cursor/execute-tool.test.ts @@ -3,6 +3,7 @@ */ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ downloadCursorArtifact: vi.fn() })) @@ -15,7 +16,7 @@ vi.mock('@/lib/internal/cursor/operations', () => ({ })) import { CursorOperationError } from '@/lib/internal/cursor/errors' -import { executeCursorTool } from '@/lib/internal/cursor/execute-tool' +import { executeCursorTool as executeCursorToolOperation } from '@/lib/internal/cursor/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' function request(overrides: Partial = {}): InternalToolOperationCall { @@ -29,6 +30,14 @@ function request(overrides: Partial = {}): InternalTo } } +async function executeCursorTool( + request: Parameters[0] +): Promise { + const result = await executeCursorToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeCursorTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -45,13 +54,26 @@ describe('executeCursorTool', () => { const response = await executeCursorTool(request({ toolId, signal: controller.signal })) expect(response.status).toBe(200) - expect(mocks.downloadCursorArtifact).toHaveBeenCalledWith( + const expectedArgs: unknown[] = [ { apiKey: 'cursor-key', agentId: 'agent-1', path: '/src/index.ts' }, - { requestId: 'request-1', signal: controller.signal } - ) + { requestId: 'request-1', signal: controller.signal }, + ] + if (toolId.endsWith('_v2')) expectedArgs.push('v2') + expect(mocks.downloadCursorArtifact.mock.calls[0]).toEqual(expectedArgs) } ) + it('forwards file bytes without serializing the file result', async () => { + const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ success: true, output: { file } }) + ) + mocks.downloadCursorArtifact.mockResolvedValueOnce(fileResult) + expect( + await executeCursorToolOperation(request({ toolId: 'cursor_download_artifact_v2' })) + ).toBe(fileResult) + }) + it('rejects invalid input before provider work', async () => { const response = await executeCursorTool(request({ input: { apiKey: '' } })) diff --git a/apps/sim/lib/internal/cursor/execute-tool.ts b/apps/sim/lib/internal/cursor/execute-tool.ts index 9436106fdec..eba25730a9d 100644 --- a/apps/sim/lib/internal/cursor/execute-tool.ts +++ b/apps/sim/lib/internal/cursor/execute-tool.ts @@ -4,7 +4,11 @@ import { cursorOperationErrorMessage, downloadCursorArtifact, } from '@/lib/internal/cursor/operations' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' const inputSchema = z.object({ apiKey: z.string().min(1, 'API key is required'), @@ -12,7 +16,9 @@ const inputSchema = z.object({ path: z.string().min(1, 'Artifact path is required'), }) -export const executeCursorTool: InternalToolOperationHandler = async (request) => { +export const executeCursorTool: InternalToolOperationHandler = async ( + request +) => { request.signal?.throwIfAborted() if ( request.toolId !== 'cursor_download_artifact' && @@ -29,12 +35,15 @@ export const executeCursorTool: InternalToolOperationHandler = async (request) = } try { - return Response.json( - await downloadCursorArtifact(parsed.data, { - requestId: request.requestId, - signal: request.signal, - }) - ) + const context = { + requestId: request.requestId, + signal: request.signal, + } + const result = + request.toolId === 'cursor_download_artifact_v2' + ? await downloadCursorArtifact(parsed.data, context, 'v2') + : await downloadCursorArtifact(parsed.data, context) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() const status = error instanceof CursorOperationError ? error.status : 500 diff --git a/apps/sim/lib/internal/cursor/operations.test.ts b/apps/sim/lib/internal/cursor/operations.test.ts index 1eff865fe46..9909d7a9bbd 100644 --- a/apps/sim/lib/internal/cursor/operations.test.ts +++ b/apps/sim/lib/internal/cursor/operations.test.ts @@ -15,6 +15,17 @@ vi.mock('@/lib/core/security/input-validation.server', () => ({ import { downloadCursorArtifact } from '@/lib/internal/cursor/operations' +const storedFile = { + id: 'stored-file', + name: 'stored.bin', + size: 5, + type: 'application/octet-stream', + mimeType: 'application/octet-stream', + url: '/api/files/stored', + key: 'execution/workspace/workflow/run/stored.bin', + context: 'execution', +} as const + describe('downloadCursorArtifact', () => { beforeEach(() => { vi.clearAllMocks() @@ -34,7 +45,8 @@ describe('downloadCursorArtifact', () => { const result = await downloadCursorArtifact( { apiKey: 'cursor-key', agentId: 'agent-1', path: '/src/index.ts' }, - { requestId: 'request-1', signal: controller.signal } + { requestId: 'request-1', signal: controller.signal }, + 'v2' ) expect(fetchMock).toHaveBeenCalledOnce() @@ -47,11 +59,34 @@ describe('downloadCursorArtifact', () => { '203.0.113.1', { profile: 'contentFetch', signal: controller.signal } ) - expect(result.output.file).toEqual({ - name: 'index.ts', - mimeType: 'text/plain', - data: Buffer.from('artifact').toString('base64'), - size: 8, + expect(result.files).toEqual([ + { name: 'index.ts', mimeType: 'text/plain', buffer: Buffer.from('artifact') }, + ]) + expect(result.present([storedFile])).toMatchObject({ + success: true, + output: { file: storedFile }, + }) + }) + + it('preserves inline file data for the legacy tool', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(Response.json({ url: 'https://download.example/artifact' })) + ) + const result = await downloadCursorArtifact( + { apiKey: 'cursor-key', agentId: 'agent-1', path: '/src/index.ts' }, + { requestId: 'request-1' } + ) + expect(result).toEqual({ + success: true, + output: { + file: { + name: 'index.ts', + mimeType: 'text/plain', + data: Buffer.from('artifact').toString('base64'), + size: 8, + }, + }, }) }) }) diff --git a/apps/sim/lib/internal/cursor/operations.ts b/apps/sim/lib/internal/cursor/operations.ts index 3365cd7969d..4ac9ba2cc81 100644 --- a/apps/sim/lib/internal/cursor/operations.ts +++ b/apps/sim/lib/internal/cursor/operations.ts @@ -9,6 +9,10 @@ import { readResponseTextWithLimit, } from '@/lib/core/utils/stream-limits' import { CursorOperationError } from '@/lib/internal/cursor/errors' +import { + createInternalToolFileResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import type { DownloadArtifactParams } from '@/tools/cursor/types' const logger = createLogger('CursorOperations') @@ -25,13 +29,32 @@ export interface CursorOperationContext { signal?: AbortSignal } -export async function downloadCursorArtifact( +interface LegacyCursorArtifactResult { + success: boolean + output: { + file: { + name: string + mimeType: string + data: string + size: number + } + } +} + +export function downloadCursorArtifact( input: DownloadArtifactParams, context: CursorOperationContext -): Promise<{ - success: true - output: { file: { name: string; mimeType: string; data: string; size: number } } -}> { +): Promise +export function downloadCursorArtifact( + input: DownloadArtifactParams, + context: CursorOperationContext, + version: 'v2' +): Promise +export async function downloadCursorArtifact( + input: DownloadArtifactParams, + context: CursorOperationContext, + version: 'v1' | 'v2' = 'v1' +): Promise { context.signal?.throwIfAborted() const authHeader = `Basic ${Buffer.from(`${input.apiKey}:`).toString('base64')}` const artifactResponse = await fetch( @@ -86,15 +109,30 @@ export async function downloadCursorArtifact( const file = { name: input.path.split('/').pop() || 'artifact', mimeType: downloadResponse.headers.get('content-type') || 'application/octet-stream', - data: fileBuffer.toString('base64'), - size: fileBuffer.length, + buffer: fileBuffer, } logger.info(`[${context.requestId}] Cursor artifact downloaded`, { agentId: input.agentId, path: input.path, - size: file.size, + size: fileBuffer.length, }) - return { success: true, output: { file } } + if (version === 'v1') { + return { + success: true, + output: { + file: { + name: file.name, + mimeType: file.mimeType, + data: fileBuffer.toString('base64'), + size: fileBuffer.length, + }, + }, + } + } + return createInternalToolFileResult(file, (storedFile) => ({ + success: true, + output: { file: storedFile }, + })) } export function cursorOperationErrorMessage(error: unknown): string { diff --git a/apps/sim/lib/internal/discord/execute-tool.ts b/apps/sim/lib/internal/discord/execute-tool.ts index 023a0be5a79..a3bc4aa7fb6 100644 --- a/apps/sim/lib/internal/discord/execute-tool.ts +++ b/apps/sim/lib/internal/discord/execute-tool.ts @@ -5,7 +5,11 @@ import { DEFAULT_MAX_JSON_BODY_BYTES } from '@/lib/api/server/validation' import { DiscordOperationError } from '@/lib/internal/discord/errors' import { executeDiscordSendMessage } from '@/lib/internal/discord/operations' import { discordSendMessageInputSchema } from '@/lib/internal/discord/schema' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' const logger = createLogger('DiscordToolExecution') @@ -26,7 +30,9 @@ function inputSizeError(input: unknown): Response | null { : null } -export const executeDiscordTool: InternalToolOperationHandler = async (request) => { +export const executeDiscordTool: InternalToolOperationHandler = async ( + request +) => { request.signal?.throwIfAborted() if (request.toolId !== 'discord_send_message') { return Response.json( @@ -54,7 +60,7 @@ export const executeDiscordTool: InternalToolOperationHandler = async (request) userId, }) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof DiscordOperationError) { diff --git a/apps/sim/lib/internal/discord/operations.test.ts b/apps/sim/lib/internal/discord/operations.test.ts index 6dfac7443b6..2843a8d2746 100644 --- a/apps/sim/lib/internal/discord/operations.test.ts +++ b/apps/sim/lib/internal/discord/operations.test.ts @@ -2,6 +2,7 @@ * @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const fileMocks = vi.hoisted(() => ({ assertToolFileAccess: vi.fn(), @@ -96,25 +97,39 @@ describe('executeDiscordSendMessage', () => { return { id: 'message-1', content: 'hello' } }) - await expect( - executeDiscordSendMessage( - { - botToken: 'bot-token', - channelId: '123', - content: 'hello', - files: [{ key: 'workspace/file.txt', name: 'file.txt', size: 4 }], - }, - { - requestId: 'request-1', - signal: controller.signal, - userId: 'user-1', - } - ) - ).resolves.toMatchObject({ + const result = await executeDiscordSendMessage( + { + botToken: 'bot-token', + channelId: '123', + content: 'hello', + files: [{ key: 'workspace/file.txt', name: 'file.txt', size: 4 }], + }, + { + requestId: 'request-1', + signal: controller.signal, + userId: 'user-1', + } + ) + if (!isInternalToolFileResult(result)) throw new Error('Expected a file output') + expect(result.files).toEqual([ + { name: 'file.txt', mimeType: 'text/plain', buffer: Buffer.from('file') }, + ]) + const storedFile = { + id: 'stored', + name: 'file.txt', + size: 4, + type: 'text/plain', + mimeType: 'text/plain', + url: '/api/files/stored', + key: 'execution/file.txt', + context: 'execution' as const, + } + expect(result.present([storedFile])).toMatchObject({ success: true, output: { data: { id: 'message-1', content: 'hello' }, fileCount: 1, + files: [storedFile], message: 'hello', }, }) diff --git a/apps/sim/lib/internal/discord/operations.ts b/apps/sim/lib/internal/discord/operations.ts index 00a5d2211eb..a4571e27c66 100644 --- a/apps/sim/lib/internal/discord/operations.ts +++ b/apps/sim/lib/internal/discord/operations.ts @@ -6,6 +6,7 @@ import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { sendDiscordMessage } from '@/lib/internal/discord/client' import { DiscordOperationError } from '@/lib/internal/discord/errors' import type { DiscordSendMessageInput } from '@/lib/internal/discord/schema' +import { createInternalToolFilesResult } from '@/lib/internal/tool-operations/file-result' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { docNotReadyMessage, isDocNotReadyError } from '@/lib/uploads/utils/doc-not-ready' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -92,8 +93,7 @@ export async function executeDiscordSendMessage( return { name: file.name, mimeType, - data: downloaded.buffer.toString('base64'), - size: downloaded.buffer.length, + buffer: downloaded.buffer, } }) const data = await sendDiscordMessage( @@ -103,13 +103,13 @@ export async function executeDiscordSendMessage( 'multipart', context.signal ) - return { + return createInternalToolFilesResult(files, (storedFiles) => ({ success: true, output: { message: typeof data.content === 'string' ? data.content : undefined, data, fileCount: userFiles.length, - files, + files: storedFiles, }, - } + })) } diff --git a/apps/sim/lib/internal/google-drive/execute-tool.test.ts b/apps/sim/lib/internal/google-drive/execute-tool.test.ts index 2081e5db43f..5fcc8255c29 100644 --- a/apps/sim/lib/internal/google-drive/execute-tool.test.ts +++ b/apps/sim/lib/internal/google-drive/execute-tool.test.ts @@ -3,6 +3,7 @@ */ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ download: vi.fn(), @@ -20,7 +21,7 @@ vi.mock('@/lib/internal/google-drive/operations', () => ({ import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { GoogleDriveOperationError } from '@/lib/internal/google-drive/errors' -import { executeGoogleDriveTool } from '@/lib/internal/google-drive/execute-tool' +import { executeGoogleDriveTool as executeGoogleDriveToolOperation } from '@/lib/internal/google-drive/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' const INPUTS = { @@ -64,6 +65,14 @@ function request( } } +async function executeGoogleDriveTool( + request: Parameters[0] +): Promise { + const result = await executeGoogleDriveToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeGoogleDriveTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -88,6 +97,15 @@ describe('executeGoogleDriveTool', () => { } ) + it('forwards file bytes without serializing the file result', async () => { + const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ success: true, output: { file } }) + ) + mocks.download.mockResolvedValueOnce(fileResult) + expect(await executeGoogleDriveToolOperation(request('google_drive_download'))).toBe(fileResult) + }) + it('preserves validation and provider error envelopes', async () => { const invalid = await executeGoogleDriveTool( request('google_drive_export', { input: { accessToken: 'token' } }) diff --git a/apps/sim/lib/internal/google-drive/execute-tool.ts b/apps/sim/lib/internal/google-drive/execute-tool.ts index d96ac227fdd..3d76c819c7c 100644 --- a/apps/sim/lib/internal/google-drive/execute-tool.ts +++ b/apps/sim/lib/internal/google-drive/execute-tool.ts @@ -20,9 +20,11 @@ import { executeGoogleDriveUpload, type GoogleDriveOperationContext, } from '@/lib/internal/google-drive/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import type { InternalToolOperationCall, InternalToolOperationHandler, + InternalToolOperationResult, } from '@/lib/internal/tool-operations/types' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' @@ -92,7 +94,9 @@ function unexpectedResponse(request: InternalToolOperationCall, error: unknown): return Response.json({ success: false, error: message }, { status }) } -export const executeGoogleDriveTool: InternalToolOperationHandler = async (request) => { +export const executeGoogleDriveTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() let serialized: string try { @@ -118,7 +122,9 @@ export const executeGoogleDriveTool: InternalToolOperationHandler = async (reque userId: request.context.userId, }) request.signal?.throwIfAborted() - return result instanceof Response ? result : Response.json(result) + return result instanceof Response || isInternalToolFileResult(result) + ? result + : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof GoogleDriveOperationError) { diff --git a/apps/sim/lib/internal/google-drive/operations.test.ts b/apps/sim/lib/internal/google-drive/operations.test.ts index bd3046b17cb..aaab39e66fb 100644 --- a/apps/sim/lib/internal/google-drive/operations.test.ts +++ b/apps/sim/lib/internal/google-drive/operations.test.ts @@ -49,6 +49,17 @@ const context = { userId: 'user-1', } +const storedFile = { + id: 'stored-file', + name: 'stored.bin', + size: 5, + type: 'application/octet-stream', + mimeType: 'application/octet-stream', + url: '/api/files/stored', + key: 'execution/workspace/workflow/run/stored.bin', + context: 'execution', +} as const + describe('Google Drive operations', () => { beforeEach(() => { vi.clearAllMocks() @@ -82,11 +93,12 @@ describe('Google Drive operations', () => { maxResponseBytes: MAX_FILE_SIZE, signal: context.signal, }) - expect(result.output.file).toEqual({ - name: 'report.pdf', - mimeType: 'application/pdf', - data: 'AAAAAA==', - size: 4, + expect(result.files).toEqual([ + { name: 'report.pdf', mimeType: 'application/pdf', buffer: Buffer.alloc(4) }, + ]) + expect(result.present([storedFile])).toMatchObject({ + success: true, + output: { file: storedFile }, }) }) @@ -112,7 +124,9 @@ describe('Google Drive operations', () => { label: 'revisionsUrl', signal: context.signal, }) - expect(result.output.metadata.revisions).toEqual([{ id: 'rev-1' }]) + expect(result.present([storedFile])).toMatchObject({ + output: { metadata: { revisions: [{ id: 'rev-1' }] } }, + }) }) it('preserves the export byte limit and exact error', async () => { diff --git a/apps/sim/lib/internal/google-drive/operations.ts b/apps/sim/lib/internal/google-drive/operations.ts index c7d02d82f26..e07fc6b552c 100644 --- a/apps/sim/lib/internal/google-drive/operations.ts +++ b/apps/sim/lib/internal/google-drive/operations.ts @@ -17,6 +17,7 @@ import type { GoogleDriveMoveInput, GoogleDriveUploadInput, } from '@/lib/internal/google-drive/input' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' import type { GoogleDriveFile, GoogleDriveRevision } from '@/tools/google_drive/types' import { @@ -204,18 +205,14 @@ export async function executeGoogleDriveDownload( } context.signal?.throwIfAborted() - return { - success: true, - output: { - file: { - name: input.fileName || metadata.name || 'download', - mimeType: finalMimeType, - data: fileBuffer.toString('base64'), - size: fileBuffer.length, - }, - metadata, + return createInternalToolFileResult( + { + buffer: fileBuffer, + name: input.fileName || metadata.name || 'download', + mimeType: finalMimeType, }, - } + (file) => ({ success: true, output: { file, metadata } }) + ) } export async function executeGoogleDriveExport( @@ -279,18 +276,14 @@ export async function executeGoogleDriveExport( ) } const fileBuffer = Buffer.from(arrayBuffer) - return { - success: true, - output: { - file: { - name: input.fileName || metadata.name || 'export', - mimeType: input.mimeType, - data: fileBuffer.toString('base64'), - size: fileBuffer.length, - }, - exportedMimeType: input.mimeType, + return createInternalToolFileResult( + { + buffer: fileBuffer, + name: input.fileName || metadata.name || 'export', + mimeType: input.mimeType, }, - } + (file) => ({ success: true, output: { file, exportedMimeType: input.mimeType } }) + ) } export async function executeGoogleDriveMove( diff --git a/apps/sim/lib/internal/google-vault/execute-tool.test.ts b/apps/sim/lib/internal/google-vault/execute-tool.test.ts index cebe945359a..c6f86679f64 100644 --- a/apps/sim/lib/internal/google-vault/execute-tool.test.ts +++ b/apps/sim/lib/internal/google-vault/execute-tool.test.ts @@ -3,6 +3,7 @@ */ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ downloadGoogleVaultExportFile: vi.fn() })) @@ -10,15 +11,45 @@ vi.mock('@/lib/internal/google-vault/operations', () => ({ downloadGoogleVaultExportFile: mocks.downloadGoogleVaultExportFile, })) -import { executeGoogleVaultTool } from '@/lib/internal/google-vault/execute-tool' +import { executeGoogleVaultTool as executeGoogleVaultToolOperation } from '@/lib/internal/google-vault/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' +async function executeGoogleVaultTool( + request: Parameters[0] +): Promise { + const result = await executeGoogleVaultToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeGoogleVaultTool', () => { beforeEach(() => { vi.clearAllMocks() mocks.downloadGoogleVaultExportFile.mockResolvedValue({ success: true, output: {} }) }) + it('forwards file bytes without serializing the file result', async () => { + const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ success: true, output: { file } }) + ) + mocks.downloadGoogleVaultExportFile.mockResolvedValueOnce(fileResult) + expect( + await executeGoogleVaultToolOperation({ + toolId: 'google_vault_download_export_file', + input: { + accessToken: 'token', + matterId: 'matter-1', + bucketName: 'bucket', + objectName: 'file.zip', + }, + headers: new Headers(), + context: createExecutionContext(), + requestId: 'request-1', + }) + ).toBe(fileResult) + }) + it('dispatches typed input and cancellation without HTTP metadata', async () => { const controller = new AbortController() const request: InternalToolOperationCall = { diff --git a/apps/sim/lib/internal/google-vault/execute-tool.ts b/apps/sim/lib/internal/google-vault/execute-tool.ts index c18a7c55327..665b423b162 100644 --- a/apps/sim/lib/internal/google-vault/execute-tool.ts +++ b/apps/sim/lib/internal/google-vault/execute-tool.ts @@ -3,7 +3,11 @@ import { z } from 'zod' import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { GoogleVaultOperationError } from '@/lib/internal/google-vault/errors' import { downloadGoogleVaultExportFile } from '@/lib/internal/google-vault/operations' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' const inputSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), @@ -13,7 +17,9 @@ const inputSchema = z.object({ fileName: z.string().optional(), }) -export const executeGoogleVaultTool: InternalToolOperationHandler = async (request) => { +export const executeGoogleVaultTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() if (request.toolId !== 'google_vault_download_export_file') { return Response.json( @@ -26,9 +32,8 @@ export const executeGoogleVaultTool: InternalToolOperationHandler = async (reque return Response.json({ success: false, error: 'Invalid request data' }, { status: 400 }) } try { - return Response.json( - await downloadGoogleVaultExportFile(parsed.data, { signal: request.signal }) - ) + const result = await downloadGoogleVaultExportFile(parsed.data, { signal: request.signal }) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() const status = isPayloadSizeLimitError(error) diff --git a/apps/sim/lib/internal/google-vault/operations.test.ts b/apps/sim/lib/internal/google-vault/operations.test.ts index fb7d34b9045..03daa3b8da5 100644 --- a/apps/sim/lib/internal/google-vault/operations.test.ts +++ b/apps/sim/lib/internal/google-vault/operations.test.ts @@ -16,6 +16,17 @@ vi.mock('@/lib/core/security/input-validation.server', () => ({ import { downloadGoogleVaultExportFile } from '@/lib/internal/google-vault/operations' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' +const storedFile = { + id: 'stored-file', + name: 'stored.bin', + size: 5, + type: 'application/octet-stream', + mimeType: 'application/octet-stream', + url: '/api/files/stored', + key: 'execution/workspace/workflow/run/stored.bin', + context: 'execution', +} as const + describe('downloadGoogleVaultExportFile', () => { beforeEach(() => { vi.clearAllMocks() @@ -53,11 +64,12 @@ describe('downloadGoogleVaultExportFile', () => { signal: controller.signal, } ) - expect(result.output.file).toEqual({ - name: 'vault export.zip', - mimeType: 'application/zip', - data: 'AQID', - size: 3, + expect(result.files).toEqual([ + { name: 'vault export.zip', mimeType: 'application/zip', buffer: Buffer.from([1, 2, 3]) }, + ]) + expect(result.present([storedFile])).toMatchObject({ + success: true, + output: { file: storedFile }, }) }) }) diff --git a/apps/sim/lib/internal/google-vault/operations.ts b/apps/sim/lib/internal/google-vault/operations.ts index 87683ae4b2b..045a55f4096 100644 --- a/apps/sim/lib/internal/google-vault/operations.ts +++ b/apps/sim/lib/internal/google-vault/operations.ts @@ -8,6 +8,7 @@ import { readResponseToBufferWithLimit, } from '@/lib/core/utils/stream-limits' import { GoogleVaultOperationError } from '@/lib/internal/google-vault/errors' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import type { GoogleVaultDownloadExportFileParams } from '@/tools/google_vault/types' import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' @@ -84,10 +85,8 @@ export async function downloadGoogleVaultExportFile( input.fileName, input.objectName ) - return { + return createInternalToolFileResult({ buffer, name, mimeType }, (file) => ({ success: true, - output: { - file: { name, mimeType, data: buffer.toString('base64'), size: buffer.length }, - }, - } + output: { file }, + })) } diff --git a/apps/sim/lib/internal/jupyter/client.test.ts b/apps/sim/lib/internal/jupyter/client.test.ts index 9f2256bf54e..d6e46c42e43 100644 --- a/apps/sim/lib/internal/jupyter/client.test.ts +++ b/apps/sim/lib/internal/jupyter/client.test.ts @@ -14,7 +14,11 @@ vi.mock('@/lib/core/security/input-validation.server', () => ({ secureFetchWithPinnedIP: securityMocks.secureFetchWithPinnedIP, })) -import { InvalidJupyterTargetError, requestJupyterApi } from '@/lib/internal/jupyter/client' +import { + InvalidJupyterTargetError, + requestJupyterApi, + requestJupyterFile, +} from '@/lib/internal/jupyter/client' describe('Jupyter client', () => { beforeEach(() => { @@ -109,4 +113,51 @@ describe('Jupyter client', () => { ).rejects.toMatchObject({ name: 'AbortError' }) expect(securityMocks.validateUrlWithDNS).not.toHaveBeenCalled() }) + + it('downloads raw bytes with token auth, the server base path, and a separate 100 MiB cap', async () => { + const controller = new AbortController() + await requestJupyterFile( + { + serverUrl: 'https://jupyter.example.com/user/alice/', + token: 'secret-token', + path: 'datasets/report #1.xlsx', + }, + controller.signal + ) + + const url = + 'https://jupyter.example.com/user/alice/files/datasets/report%20%231.xlsx?download=1' + expect(securityMocks.validateUrlWithDNS).toHaveBeenCalledWith( + url, + 'serverUrl', + 'selfHostedService' + ) + expect(securityMocks.secureFetchWithPinnedIP).toHaveBeenCalledWith(url, '192.0.2.10', { + method: 'GET', + headers: { Authorization: 'token secret-token' }, + body: undefined, + profile: 'selfHostedService', + maxRedirects: 0, + maxResponseBytes: 100 * 1024 * 1024, + signal: controller.signal, + }) + }) + + it.each(['../secret', '%2e%2e/secret', 'data/../secret'])( + 'rejects raw download traversal before DNS: %s', + async (path) => { + await expect( + requestJupyterFile({ serverUrl: 'jupyter.example.com', token: 'token', path }) + ).rejects.toMatchObject({ name: 'UnsafeJupyterPathError' }) + expect(securityMocks.validateUrlWithDNS).not.toHaveBeenCalled() + } + ) + + it('rejects a raw file target blocked by DNS policy', async () => { + securityMocks.validateUrlWithDNS.mockResolvedValue({ isValid: false, error: 'blocked' }) + await expect( + requestJupyterFile({ serverUrl: 'jupyter.example.com', token: 'token', path: 'data.csv' }) + ).rejects.toBeInstanceOf(InvalidJupyterTargetError) + expect(securityMocks.secureFetchWithPinnedIP).not.toHaveBeenCalled() + }) }) diff --git a/apps/sim/lib/internal/jupyter/client.ts b/apps/sim/lib/internal/jupyter/client.ts index 3e1b3acc3cb..06348db6b29 100644 --- a/apps/sim/lib/internal/jupyter/client.ts +++ b/apps/sim/lib/internal/jupyter/client.ts @@ -7,9 +7,11 @@ import { } from '@/lib/core/security/input-validation.server' import { buildJupyterAuthHeaders, + encodeJupyterPath, InvalidJupyterServerUrlError, normalizeJupyterServerUrl, } from '@/lib/internal/jupyter/protocol' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' export class InvalidJupyterTargetError extends Error { constructor(message: string) { @@ -30,6 +32,30 @@ export interface JupyterApiRequest { export async function requestJupyterApi( input: JupyterApiRequest, signal?: AbortSignal +): Promise { + return requestJupyter(input, `api/${input.path}`, MAX_JSON_API_RESPONSE_BYTES, signal) +} + +/** Downloads raw file bytes through Jupyter's authenticated `/files/` handler. */ +export async function requestJupyterFile( + input: Pick, + signal?: AbortSignal +): Promise { + signal?.throwIfAborted() + const path = encodeJupyterPath(input.path) + return requestJupyter( + { serverUrl: input.serverUrl, token: input.token, path: input.path, method: 'GET' }, + `files/${path}?download=1`, + MAX_BUFFERED_TRANSFER_BYTES, + signal + ) +} + +async function requestJupyter( + input: JupyterApiRequest, + route: string, + maxResponseBytes: number, + signal?: AbortSignal ): Promise { signal?.throwIfAborted() let base: string @@ -41,7 +67,7 @@ export async function requestJupyterApi( } throw error } - const url = `${base}/api/${input.path}` + const url = `${base}/${route}` const urlValidation = await validateUrlWithDNS(url, 'serverUrl', 'selfHostedService') signal?.throwIfAborted() @@ -59,7 +85,7 @@ export async function requestJupyterApi( body: hasBody ? JSON.stringify(input.body) : undefined, profile: 'selfHostedService', maxRedirects: 0, - maxResponseBytes: MAX_JSON_API_RESPONSE_BYTES, + maxResponseBytes, signal, }) } diff --git a/apps/sim/lib/internal/jupyter/execute-tool.test.ts b/apps/sim/lib/internal/jupyter/execute-tool.test.ts index e956ae7e1d5..69464995721 100644 --- a/apps/sim/lib/internal/jupyter/execute-tool.test.ts +++ b/apps/sim/lib/internal/jupyter/execute-tool.test.ts @@ -7,11 +7,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const operationMocks = vi.hoisted(() => ({ executeJupyterProxy: vi.fn(), executeJupyterUpload: vi.fn(), + executeJupyterGetContent: vi.fn(), })) vi.mock('@/lib/internal/jupyter/operations', () => operationMocks) +import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { executeJupyterTool, JUPYTER_PROXY_TOOL_IDS } from '@/lib/internal/jupyter/execute-tool' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' const PROXY_BODY = { @@ -38,6 +41,12 @@ function createRequest( } } +async function executeResponse(request: InternalToolOperationCall): Promise { + const response = await executeJupyterTool(request) + if (!(response instanceof Response)) throw new Error('Expected a JSON response') + return response +} + describe('executeJupyterTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -50,7 +59,7 @@ describe('executeJupyterTool', () => { }) it.each(JUPYTER_PROXY_TOOL_IDS)('recognizes proxy tool ID %s', async (toolId) => { - const response = await executeJupyterTool(createRequest({ toolId })) + const response = await executeResponse(createRequest({ toolId })) expect(response.status).toBe(200) expect(operationMocks.executeJupyterProxy).toHaveBeenCalledWith(PROXY_BODY, { @@ -60,7 +69,7 @@ describe('executeJupyterTool', () => { }) it('validates the canonical proxy contract before provider work', async () => { - const response = await executeJupyterTool(createRequest({ input: { ...PROXY_BODY, path: '' } })) + const response = await executeResponse(createRequest({ input: { ...PROXY_BODY, path: '' } })) expect(response.status).toBe(400) await expect(response.json()).resolves.toMatchObject({ @@ -79,7 +88,7 @@ describe('executeJupyterTool', () => { fileName: 'hello.txt', } - const response = await executeJupyterTool( + const response = await executeResponse( createRequest({ toolId: 'jupyter_upload_file', input, @@ -96,7 +105,7 @@ describe('executeJupyterTool', () => { }) it('fails upload closed without a trusted execution user', async () => { - const response = await executeJupyterTool( + const response = await executeResponse( createRequest({ toolId: 'jupyter_upload_file', context: { @@ -122,11 +131,50 @@ describe('executeJupyterTool', () => { }) it('returns a deterministic error for unsupported IDs', async () => { - const response = await executeJupyterTool(createRequest({ toolId: 'jupyter_unknown' })) + const response = await executeResponse(createRequest({ toolId: 'jupyter_unknown' })) expect(response.status).toBe(500) await expect(response.json()).resolves.toEqual({ error: 'Unsupported Jupyter tool: jupyter_unknown', }) }) + + it('preserves the typed v2 file result for central storage', async () => { + const result = createInternalToolFileResult( + { buffer: Buffer.from('hello'), name: 'notes.txt', mimeType: 'text/plain' }, + (file) => ({ success: true, output: { file } }) + ) + const input = { ...PROXY_BODY, path: 'notes.txt' } + const controller = new AbortController() + operationMocks.executeJupyterGetContent.mockResolvedValue(result) + expect( + await executeJupyterTool( + createRequest({ toolId: 'jupyter_get_content_v2', input, signal: controller.signal }) + ) + ).toBe(result) + expect(operationMocks.executeJupyterGetContent).toHaveBeenCalledWith(input, { + requestId: 'request-1', + signal: controller.signal, + }) + expect(operationMocks.executeJupyterProxy).not.toHaveBeenCalled() + }) + + it('rejects non-GET v2 reads before provider work', async () => { + const response = await executeResponse( + createRequest({ toolId: 'jupyter_get_content_v2', input: { ...PROXY_BODY, method: 'POST' } }) + ) + expect(response.status).toBe(400) + expect(operationMocks.executeJupyterGetContent).not.toHaveBeenCalled() + }) + + it('projects a download byte limit failure as 413', async () => { + operationMocks.executeJupyterGetContent.mockRejectedValue( + new PayloadSizeLimitError({ label: 'Jupyter file download', maxBytes: 100 * 1024 * 1024 }) + ) + const response = await executeResponse(createRequest({ toolId: 'jupyter_get_content_v2' })) + expect(response.status).toBe(413) + await expect(response.json()).resolves.toMatchObject({ + error: expect.stringContaining('Jupyter file download exceeds maximum size'), + }) + }) }) diff --git a/apps/sim/lib/internal/jupyter/execute-tool.ts b/apps/sim/lib/internal/jupyter/execute-tool.ts index 26ba77d704b..d732610cb30 100644 --- a/apps/sim/lib/internal/jupyter/execute-tool.ts +++ b/apps/sim/lib/internal/jupyter/execute-tool.ts @@ -4,9 +4,19 @@ import type { AnyApiRouteContract } from '@/lib/api/contracts' import { jupyterUploadContract } from '@/lib/api/contracts/storage-transfer' import { jupyterProxyContract } from '@/lib/api/contracts/tools/jupyter' import { DEFAULT_MAX_JSON_BODY_BYTES } from '@/lib/api/server/validation' -import { executeJupyterProxy, executeJupyterUpload } from '@/lib/internal/jupyter/operations' +import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { InvalidJupyterTargetError } from '@/lib/internal/jupyter/client' +import { + executeJupyterGetContent, + executeJupyterProxy, + executeJupyterUpload, +} from '@/lib/internal/jupyter/operations' +import { UnsafeJupyterPathError } from '@/lib/internal/jupyter/protocol' import { parseInternalToolInput } from '@/lib/internal/tool-operations/parse-input' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' const proxyLogger = createLogger('JupyterProxyAPI') const uploadLogger = createLogger('JupyterUploadAPI') @@ -53,15 +63,31 @@ function unexpectedErrorResponse( } /** Executes every Jupyter tool without routing through the application's HTTP listener. */ -export const executeJupyterTool: InternalToolOperationHandler = async ({ - toolId, - input, - context, - requestId, - signal, -}) => { +export const executeJupyterTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async ({ toolId, input, context, requestId, signal }) => { signal?.throwIfAborted() + if (toolId === 'jupyter_get_content_v2') { + const parsed = parseJupyterBody(jupyterProxyContract, input) + if (!parsed.success) return parsed.response + if (parsed.data.method !== 'GET') { + return Response.json({ error: 'Get Content requires a GET request' }, { status: 400 }) + } + try { + return await executeJupyterGetContent(parsed.data, { requestId, signal }) + } catch (error) { + signal?.throwIfAborted() + if (error instanceof UnsafeJupyterPathError || error instanceof InvalidJupyterTargetError) { + return Response.json({ error: error.message }, { status: 400 }) + } + if (isPayloadSizeLimitError(error)) { + return Response.json({ error: error.message }, { status: 413 }) + } + return unexpectedErrorResponse('proxy', requestId, error, signal) + } + } + if (JUPYTER_PROXY_TOOL_ID_SET.has(toolId)) { const parsed = parseJupyterBody(jupyterProxyContract, input) if (!parsed.success) return parsed.response diff --git a/apps/sim/lib/internal/jupyter/get-content.test.ts b/apps/sim/lib/internal/jupyter/get-content.test.ts new file mode 100644 index 00000000000..6f73d7fd3b0 --- /dev/null +++ b/apps/sim/lib/internal/jupyter/get-content.test.ts @@ -0,0 +1,179 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const clientMocks = vi.hoisted(() => ({ + InvalidJupyterTargetError: class extends Error {}, + requestJupyterApi: vi.fn(), + requestJupyterFile: vi.fn(), +})) + +vi.mock('@/lib/internal/jupyter/client', () => clientMocks) +vi.mock('@/lib/internal/jupyter/file-input', () => ({ resolveJupyterUploadFile: vi.fn() })) + +import { executeJupyterGetContent } from '@/lib/internal/jupyter/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' + +const INPUT = { + serverUrl: 'https://jupyter.example.com/user/alice', + token: 'token', + path: 'data/report #1.xlsx', +} +const CONTEXT = { requestId: 'request-1' } +const MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + +describe('Jupyter Get Content v2', () => { + beforeEach(() => vi.clearAllMocks()) + + it('downloads a 12 MiB workbook through the raw endpoint and presents only its stored file', async () => { + const controller = new AbortController() + const buffer = Buffer.alloc(12 * 1024 * 1024, 42) + clientMocks.requestJupyterApi.mockResolvedValue( + Response.json({ name: 'report #1.xlsx', type: 'file', size: buffer.length, content: null }) + ) + clientMocks.requestJupyterFile.mockResolvedValue( + new Response(buffer, { headers: { 'content-type': MIME_TYPE } }) + ) + + const result = await executeJupyterGetContent(INPUT, { ...CONTEXT, signal: controller.signal }) + expect(isInternalToolFileResult(result)).toBe(true) + if (!isInternalToolFileResult(result)) throw new Error('Expected a file result') + expect(result.files).toHaveLength(1) + expect(result.files[0]?.buffer.length).toBe(buffer.length) + expect(result.files[0]?.buffer.equals(buffer)).toBe(true) + expect(result.files[0]?.name).toBe('report #1.xlsx') + expect(result.files[0]?.mimeType).toBe(MIME_TYPE) + const file = { + id: 'file-1', + name: 'report #1.xlsx', + type: MIME_TYPE, + mimeType: MIME_TYPE, + size: buffer.length, + key: 'execution/file-1', + url: '/api/files/serve/execution/file-1', + } + expect(result.present([file])).toEqual({ success: true, output: { file } }) + expect(clientMocks.requestJupyterApi).toHaveBeenCalledExactlyOnceWith( + { ...INPUT, method: 'GET', path: 'contents/data/report%20%231.xlsx?content=0' }, + controller.signal + ) + expect(clientMocks.requestJupyterFile).toHaveBeenCalledExactlyOnceWith(INPUT, controller.signal) + }) + + it('returns text files as stored files without an inline text alias', async () => { + clientMocks.requestJupyterApi.mockResolvedValue( + Response.json({ type: 'file', name: 'notes.txt', size: 5 }) + ) + clientMocks.requestJupyterFile.mockResolvedValue( + new Response('hello', { headers: { 'content-type': 'text/plain; charset=UTF-8' } }) + ) + const result = await executeJupyterGetContent({ ...INPUT, path: 'notes.txt' }, CONTEXT) + if (!isInternalToolFileResult(result)) throw new Error('Expected a file result') + expect(result.files[0]?.buffer.toString('utf8')).toBe('hello') + expect(result.files[0]?.mimeType).toBe('text/plain') + }) + + it.each([ + { type: 'notebook', content: { cells: [{ cell_type: 'code', source: ['1 + 1'] }] } }, + { type: 'directory', content: [{ type: 'file', name: 'data.csv', path: 'docs/data.csv' }] }, + ])('preserves structured $type output without a raw file request', async ({ type, content }) => { + clientMocks.requestJupyterApi + .mockResolvedValueOnce(Response.json({ name: 'docs', path: 'docs', type, content: null })) + .mockResolvedValueOnce( + Response.json({ name: 'docs', path: 'docs', type, content, format: 'json', mimetype: null }) + ) + const response = await executeJupyterGetContent({ ...INPUT, path: 'docs' }, CONTEXT) + if (!(response instanceof Response)) throw new Error('Expected structured content') + await expect(response.json()).resolves.toEqual({ + success: true, + output: { + name: 'docs', + path: 'docs', + mimetype: null, + text: JSON.stringify(content), + file: null, + }, + }) + expect(clientMocks.requestJupyterApi).toHaveBeenNthCalledWith( + 2, + { ...INPUT, method: 'GET', path: `contents/docs?content=1&type=${type}` }, + undefined + ) + expect(clientMocks.requestJupyterFile).not.toHaveBeenCalled() + }) + + it('rejects oversized metadata before reading file bytes', async () => { + clientMocks.requestJupyterApi.mockResolvedValue( + Response.json({ type: 'file', size: MAX_BUFFERED_TRANSFER_BYTES + 1 }) + ) + const response = await executeJupyterGetContent(INPUT, CONTEXT) + if (!(response instanceof Response)) throw new Error('Expected a size error') + expect(response.status).toBe(413) + expect(clientMocks.requestJupyterFile).not.toHaveBeenCalled() + }) + + it('enforces the raw byte cap when metadata size is missing or wrong', async () => { + const cancel = vi.fn() + clientMocks.requestJupyterApi.mockResolvedValue(Response.json({ type: 'file', size: 1 })) + clientMocks.requestJupyterFile.mockResolvedValue( + new Response(new ReadableStream({ cancel }), { + headers: { 'content-length': String(MAX_BUFFERED_TRANSFER_BYTES + 1) }, + }) + ) + await expect(executeJupyterGetContent(INPUT, CONTEXT)).rejects.toMatchObject({ + name: 'PayloadSizeLimitError', + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + }) + expect(cancel).toHaveBeenCalledOnce() + }) + + it('keeps notebook JSON bounded at 10 MiB', async () => { + clientMocks.requestJupyterApi + .mockResolvedValueOnce(Response.json({ type: 'notebook' })) + .mockResolvedValueOnce( + new Response('', { headers: { 'content-length': String(10 * 1024 * 1024 + 1) } }) + ) + await expect(executeJupyterGetContent(INPUT, CONTEXT)).rejects.toMatchObject({ + name: 'PayloadSizeLimitError', + maxBytes: 10 * 1024 * 1024, + }) + expect(clientMocks.requestJupyterFile).not.toHaveBeenCalled() + }) + + it.each(['metadata', 'download'])( + 'preserves an upstream %s failure without returning a file', + async (stage) => { + const errorResponse = new Response('not found', { status: 404 }) + clientMocks.requestJupyterApi.mockResolvedValue( + stage === 'metadata' ? errorResponse : Response.json({ type: 'file' }) + ) + clientMocks.requestJupyterFile.mockResolvedValue(errorResponse) + const response = await executeJupyterGetContent(INPUT, CONTEXT) + if (!(response instanceof Response)) throw new Error('Expected an upstream error') + expect(response.status).toBe(404) + await expect(response.json()).resolves.toEqual({ error: 'Jupyter API error: 404 not found' }) + } + ) + + it('does not fetch raw bytes when cancellation arrives after metadata', async () => { + const controller = new AbortController() + clientMocks.requestJupyterApi.mockImplementation(async () => { + controller.abort(new DOMException('cancelled', 'AbortError')) + return Response.json({ type: 'file' }) + }) + await expect( + executeJupyterGetContent(INPUT, { ...CONTEXT, signal: controller.signal }) + ).rejects.toMatchObject({ name: 'AbortError' }) + expect(clientMocks.requestJupyterFile).not.toHaveBeenCalled() + }) + + it('rejects malformed metadata without requesting raw bytes', async () => { + clientMocks.requestJupyterApi.mockResolvedValue(Response.json({ type: 'unexpected' })) + const response = await executeJupyterGetContent(INPUT, CONTEXT) + if (!(response instanceof Response)) throw new Error('Expected an invalid model error') + expect(response.status).toBe(502) + expect(clientMocks.requestJupyterFile).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/internal/jupyter/operations.test.ts b/apps/sim/lib/internal/jupyter/operations.test.ts index b8a8d33ca7b..a705d1f6f38 100644 --- a/apps/sim/lib/internal/jupyter/operations.test.ts +++ b/apps/sim/lib/internal/jupyter/operations.test.ts @@ -8,6 +8,7 @@ const clientMocks = vi.hoisted(() => { return { InvalidJupyterTargetError, requestJupyterApi: vi.fn(), + requestJupyterFile: vi.fn(), } }) const fileInputMocks = vi.hoisted(() => ({ diff --git a/apps/sim/lib/internal/jupyter/operations.ts b/apps/sim/lib/internal/jupyter/operations.ts index a42129cd911..3beeef70bb3 100644 --- a/apps/sim/lib/internal/jupyter/operations.ts +++ b/apps/sim/lib/internal/jupyter/operations.ts @@ -1,7 +1,18 @@ import { createLogger } from '@sim/logger' import type { JupyterUploadBody } from '@/lib/api/contracts/storage-transfer' import type { JupyterProxyBody } from '@/lib/api/contracts/tools/jupyter' -import { InvalidJupyterTargetError, requestJupyterApi } from '@/lib/internal/jupyter/client' +import { MAX_JSON_API_RESPONSE_BYTES } from '@/lib/core/security/input-validation.server' +import { + DEFAULT_MAX_ERROR_BODY_BYTES, + readResponseJsonWithLimit, + readResponseTextWithLimit, + readResponseToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' +import { + InvalidJupyterTargetError, + requestJupyterApi, + requestJupyterFile, +} from '@/lib/internal/jupyter/client' import { resolveJupyterUploadFile } from '@/lib/internal/jupyter/file-input' import { assertSafeJupyterProxyPath, @@ -9,6 +20,10 @@ import { parseJupyterContentModel, UnsafeJupyterPathError, } from '@/lib/internal/jupyter/protocol' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { InternalToolOperationResult } from '@/lib/internal/tool-operations/types' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' +import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' const uploadLogger = createLogger('JupyterUploadAPI') @@ -25,6 +40,107 @@ function validationErrorResponse(error: UnsafeJupyterPathError | InvalidJupyterT return Response.json({ success: false, error: error.message }, { status: 400 }) } +/** Stores files before JSON presentation while preserving structured notebook and directory reads. */ +export async function executeJupyterGetContent( + input: Pick, + context: JupyterOperationContext +): Promise { + const { signal } = context + signal?.throwIfAborted() + const path = encodeJupyterPath(input.path) + const auth = { serverUrl: input.serverUrl, token: input.token } + const metadataResponse = await requestJupyterApi( + { ...auth, method: 'GET', path: `contents/${path}?content=0` }, + signal + ) + if (!metadataResponse.ok) return jupyterReadErrorResponse(metadataResponse, signal) + const metadata = parseJupyterContentModel( + await readResponseJsonWithLimit(metadataResponse, { + maxBytes: MAX_JSON_API_RESPONSE_BYTES, + label: 'Jupyter content metadata', + signal, + }) + ) + signal?.throwIfAborted() + if (!metadata?.type) { + return Response.json({ error: 'Jupyter returned an invalid content model' }, { status: 502 }) + } + + if (metadata.type === 'file') { + if (metadata.size !== undefined && metadata.size > MAX_BUFFERED_TRANSFER_BYTES) { + return Response.json( + { error: 'Jupyter file exceeds the 100 MB download limit' }, + { status: 413 } + ) + } + const response = await requestJupyterFile(input, signal) + if (!response.ok) return jupyterReadErrorResponse(response, signal) + const buffer = await readResponseToBufferWithLimit(response, { + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + label: 'Jupyter file download', + signal, + }) + signal?.throwIfAborted() + const name = metadata.name || input.path.split('/').filter(Boolean).at(-1) || 'file' + const mimeType = + response.headers.get('content-type')?.split(';')[0]?.trim() || + metadata.mimetype || + getMimeTypeFromExtension(getFileExtension(name)) + return createInternalToolFileResult({ buffer, name, mimeType }, (file) => ({ + success: true, + output: { file }, + })) + } + + const response = await requestJupyterApi( + { ...auth, method: 'GET', path: `contents/${path}?content=1&type=${metadata.type}` }, + signal + ) + if (!response.ok) return jupyterReadErrorResponse(response, signal) + const data = parseJupyterContentModel( + await readResponseJsonWithLimit(response, { + maxBytes: MAX_JSON_API_RESPONSE_BYTES, + label: 'Jupyter structured content', + signal, + }) + ) + signal?.throwIfAborted() + if (data?.type !== metadata.type) { + return Response.json({ error: 'Jupyter returned an invalid content model' }, { status: 502 }) + } + const text = + data.format === 'json' || typeof data.content === 'object' + ? JSON.stringify(data.content) + : typeof data.content === 'string' + ? data.content + : null + return Response.json({ + success: true, + output: { + name: data.name ?? metadata.name ?? '', + path: data.path ?? input.path, + mimetype: data.mimetype ?? null, + text, + file: null, + }, + }) +} + +async function jupyterReadErrorResponse( + response: Awaited>, + signal?: AbortSignal +): Promise { + const errorText = await readResponseTextWithLimit(response, { + maxBytes: DEFAULT_MAX_ERROR_BODY_BYTES, + label: 'Jupyter read error', + signal, + }) + return Response.json( + { error: `Jupyter API error: ${response.status} ${errorText}` }, + { status: response.status } + ) +} + /** Executes the shared Jupyter proxy contract and mirrors the upstream response verbatim. */ export async function executeJupyterProxy( input: JupyterProxyBody, diff --git a/apps/sim/lib/internal/microsoft-teams/execute-tool.test.ts b/apps/sim/lib/internal/microsoft-teams/execute-tool.test.ts index da09c978423..873618b9d72 100644 --- a/apps/sim/lib/internal/microsoft-teams/execute-tool.test.ts +++ b/apps/sim/lib/internal/microsoft-teams/execute-tool.test.ts @@ -3,6 +3,7 @@ */ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ deleteMicrosoftTeamsChatMessage: vi.fn(), @@ -16,9 +17,17 @@ vi.mock('@/lib/internal/microsoft-teams/operations', () => ({ writeMicrosoftTeamsChatMessage: mocks.writeMicrosoftTeamsChatMessage, })) -import { executeMicrosoftTeamsTool } from '@/lib/internal/microsoft-teams/execute-tool' +import { executeMicrosoftTeamsTool as executeMicrosoftTeamsToolOperation } from '@/lib/internal/microsoft-teams/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' +async function executeMicrosoftTeamsTool( + request: Parameters[0] +): Promise { + const result = await executeMicrosoftTeamsToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeMicrosoftTeamsTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -27,6 +36,23 @@ describe('executeMicrosoftTeamsTool', () => { mocks.writeMicrosoftTeamsChatMessage.mockResolvedValue({ success: true, output: {} }) }) + it('forwards file bytes without serializing the file result', async () => { + const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ success: true, output: { file } }) + ) + mocks.writeMicrosoftTeamsChatMessage.mockResolvedValueOnce(fileResult) + expect( + await executeMicrosoftTeamsToolOperation({ + toolId: 'microsoft_teams_write_chat', + input: { accessToken: 'token', chatId: 'chat-1', content: 'hello', files: null }, + headers: new Headers(), + context: createExecutionContext(), + requestId: 'request-1', + }) + ).toBe(fileResult) + }) + it('dispatches typed input with cancellation', async () => { const controller = new AbortController() const input = { accessToken: 'token', chatId: 'chat-1', messageId: 'message-1' } diff --git a/apps/sim/lib/internal/microsoft-teams/execute-tool.ts b/apps/sim/lib/internal/microsoft-teams/execute-tool.ts index 0c371d540b9..442be760b82 100644 --- a/apps/sim/lib/internal/microsoft-teams/execute-tool.ts +++ b/apps/sim/lib/internal/microsoft-teams/execute-tool.ts @@ -12,7 +12,11 @@ import { microsoftTeamsWriteChannelInputSchema, microsoftTeamsWriteChatInputSchema, } from '@/lib/internal/microsoft-teams/schema' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' const deleteInputSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), @@ -36,7 +40,9 @@ function inputSizeError(input: unknown): Response | null { ) } -export const executeMicrosoftTeamsTool: InternalToolOperationHandler = async (request) => { +export const executeMicrosoftTeamsTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() const sizeError = inputSizeError(request.input) if (sizeError) return sizeError @@ -50,12 +56,14 @@ export const executeMicrosoftTeamsTool: InternalToolOperationHandler = async (re case 'microsoft_teams_write_chat': { const parsed = microsoftTeamsWriteChatInputSchema.safeParse(request.input) if (!parsed.success) return validationErrorResponse(parsed.error) - return Response.json(await writeMicrosoftTeamsChatMessage(parsed.data, context)) + const result = await writeMicrosoftTeamsChatMessage(parsed.data, context) + return isInternalToolFileResult(result) ? result : Response.json(result) } case 'microsoft_teams_write_channel': { const parsed = microsoftTeamsWriteChannelInputSchema.safeParse(request.input) if (!parsed.success) return validationErrorResponse(parsed.error) - return Response.json(await writeMicrosoftTeamsChannelMessage(parsed.data, context)) + const result = await writeMicrosoftTeamsChannelMessage(parsed.data, context) + return isInternalToolFileResult(result) ? result : Response.json(result) } case 'microsoft_teams_delete_chat_message': { const parsed = deleteInputSchema.safeParse(request.input) diff --git a/apps/sim/lib/internal/microsoft-teams/operations.test.ts b/apps/sim/lib/internal/microsoft-teams/operations.test.ts index 356c68571f7..927769c2429 100644 --- a/apps/sim/lib/internal/microsoft-teams/operations.test.ts +++ b/apps/sim/lib/internal/microsoft-teams/operations.test.ts @@ -2,6 +2,7 @@ * @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ assertToolFileAccess: vi.fn(), @@ -54,6 +55,7 @@ describe('deleteMicrosoftTeamsChatMessage', () => { expect(mocks.fetch.mock.calls[1][1]).toEqual( expect.objectContaining({ method: 'POST', signal: controller.signal }) ) + if (isInternalToolFileResult(result)) throw new Error('Expected a JSON result') expect(result.output).toEqual({ deleted: true, messageId: 'message-1', @@ -83,6 +85,7 @@ describe('deleteMicrosoftTeamsChatMessage', () => { expect(mocks.fetch.mock.calls[0][1]).toEqual( expect.objectContaining({ method: 'POST', signal: controller.signal }) ) + if (isInternalToolFileResult(result)) throw new Error('Expected a JSON result') expect(result.output).toEqual({ updatedContent: true, metadata: { @@ -95,6 +98,50 @@ describe('deleteMicrosoftTeamsChatMessage', () => { }) }) + it('keeps multiple valid attachments out of the message response body', async () => { + const buffer = Buffer.alloc(4 * 1024 * 1024, 1) + const sourceFiles = Array.from({ length: 3 }, (_, index) => ({ + id: `source-${index}`, + key: `workspace/file-${index}.txt`, + name: `file-${index}.txt`, + size: buffer.length, + type: 'text/plain', + })) + mocks.processFilesToUserFiles.mockReturnValue(sourceFiles) + mocks.downloadServableFileFromStorage.mockResolvedValue({ buffer, contentType: 'text/plain' }) + mocks.fetch.mockReset() + for (const file of sourceFiles) { + mocks.fetch + .mockResolvedValueOnce(Response.json({ id: file.id })) + .mockResolvedValueOnce( + Response.json({ id: file.id, webDavUrl: `https://teams.example/${file.name}` }) + ) + } + mocks.fetch.mockResolvedValueOnce(Response.json({ id: 'message-1', chatId: 'chat-1' })) + + const result = await writeMicrosoftTeamsChatMessage( + { accessToken: 'token', chatId: 'chat-1', content: 'hello', files: sourceFiles }, + { requestId: 'request-1', userId: 'user-1' } + ) + + if (!isInternalToolFileResult(result)) throw new Error('Expected a file output') + expect(result.files).toHaveLength(3) + for (const file of result.files) expect(file.buffer).toBe(buffer) + expect(mocks.downloadServableFileFromStorage).toHaveBeenCalledTimes(3) + expect(mocks.fetch).toHaveBeenCalledTimes(7) + const storedFiles = sourceFiles.map((file) => ({ + ...file, + mimeType: file.type, + url: `/api/files/${file.id}`, + context: 'execution' as const, + })) + const presented = result.present(storedFiles) + expect(presented).toMatchObject({ + output: { files: storedFiles, metadata: { attachmentCount: 3 } }, + }) + expect(Buffer.byteLength(JSON.stringify(presented))).toBeLessThan(10 * 1024) + }) + it('resolves mentions in-process while preserving the enhanced output envelope', async () => { mocks.fetch.mockReset() mocks.fetch @@ -120,6 +167,7 @@ describe('deleteMicrosoftTeamsChatMessage', () => { body: { contentType: 'html', content: 'Ada hello' }, mentions: [{ id: 0, mentionText: 'Ada' }], }) + if (isInternalToolFileResult(result)) throw new Error('Expected a JSON result') expect(result.output).toMatchObject({ updatedContent: true, metadata: { chatId: 'chat-1', attachmentCount: 0 }, diff --git a/apps/sim/lib/internal/microsoft-teams/operations.ts b/apps/sim/lib/internal/microsoft-teams/operations.ts index 4de7a0cd846..7cca5977186 100644 --- a/apps/sim/lib/internal/microsoft-teams/operations.ts +++ b/apps/sim/lib/internal/microsoft-teams/operations.ts @@ -11,6 +11,10 @@ import type { MicrosoftTeamsWriteChannelInput, MicrosoftTeamsWriteChatInput, } from '@/lib/internal/microsoft-teams/schema' +import { + createInternalToolFilesResult, + type InternalToolFile, +} from '@/lib/internal/tool-operations/file-result' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { docNotReadyMessage, isDocNotReadyError } from '@/lib/uploads/utils/doc-not-ready' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -35,13 +39,6 @@ export interface MicrosoftTeamsOperationContext { userId?: string } -interface TeamsFileOutput { - name: string - mimeType: string - data: string - size: number -} - interface TeamsAttachmentRef { id: string contentType: 'reference' @@ -95,13 +92,13 @@ async function uploadFilesForMessage( rawFiles: NonNullable, client: MicrosoftTeamsClient, context: MicrosoftTeamsOperationContext -): Promise<{ attachments: TeamsAttachmentRef[]; files: TeamsFileOutput[] }> { +): Promise<{ attachments: TeamsAttachmentRef[]; files: InternalToolFile[] }> { if (rawFiles.length === 0) return { attachments: [], files: [] } if (!context.userId) throw new MicrosoftTeamsOperationError('Authentication required', 401) const requestId = context.requestId || 'microsoft-teams-operation' const userFiles = processFilesToUserFiles(rawFiles, requestId, logger) const attachments: TeamsAttachmentRef[] = [] - const files: TeamsFileOutput[] = [] + const files: InternalToolFile[] = [] let totalBytes = 0 for (const file of userFiles) { @@ -141,8 +138,7 @@ async function uploadFilesForMessage( files.push({ name: file.name, mimeType: contentType, - data: buffer.toString('base64'), - size: buffer.length, + buffer, }) let uploaded: MicrosoftTeamsGraphObject @@ -353,17 +349,20 @@ async function sendMessage(args: { 'Failed to send Teams message', args.context.signal ) - return { - success: true as const, - output: { - updatedContent: true, - metadata: { - ...args.enhancedMetadata(data), - attachmentCount: uploaded.attachments.length, - }, - files: uploaded.files, + const output = { + updatedContent: true, + metadata: { + ...args.enhancedMetadata(data), + attachmentCount: uploaded.attachments.length, }, } + if (uploaded.files.length === 0) { + return { success: true as const, output: { ...output, files: [] } } + } + return createInternalToolFilesResult(uploaded.files, (files) => ({ + success: true, + output: { ...output, files }, + })) } export async function writeMicrosoftTeamsChatMessage( diff --git a/apps/sim/lib/internal/microsoft-word/execute-tool.test.ts b/apps/sim/lib/internal/microsoft-word/execute-tool.test.ts index bee8eae27e3..f1ac631927f 100644 --- a/apps/sim/lib/internal/microsoft-word/execute-tool.test.ts +++ b/apps/sim/lib/internal/microsoft-word/execute-tool.test.ts @@ -3,6 +3,7 @@ */ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const operationMocks = vi.hoisted(() => ({ executeMicrosoftWordAppend: vi.fn(), @@ -18,7 +19,7 @@ vi.mock('@/lib/internal/microsoft-word/operations', () => operationMocks) import { DEFAULT_MAX_JSON_BODY_BYTES } from '@/lib/api/server/validation' import { GraphRequestError } from '@/lib/internal/microsoft-word/client' -import { executeMicrosoftWordTool } from '@/lib/internal/microsoft-word/execute-tool' +import { executeMicrosoftWordTool as executeMicrosoftWordToolOperation } from '@/lib/internal/microsoft-word/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' const READ_INPUT = { accessToken: 'token', documentId: 'document-1' } @@ -70,6 +71,14 @@ const TOOL_CASES = [ ], ] as const +async function executeMicrosoftWordTool( + request: Parameters[0] +): Promise { + const result = await executeMicrosoftWordToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeMicrosoftWordTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -96,6 +105,22 @@ describe('executeMicrosoftWordTool', () => { }) }) + it('forwards file bytes without serializing the file result', async () => { + const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ success: true, output: { file } }) + ) + operationMocks.executeMicrosoftWordExportPdf.mockResolvedValueOnce(fileResult) + expect( + await executeMicrosoftWordToolOperation( + createRequest({ + toolId: 'microsoft_word_export_pdf', + input: { accessToken: 'token', documentId: 'document-1' }, + }) + ) + ).toBe(fileResult) + }) + it('returns validation errors before provider work', async () => { const response = await executeMicrosoftWordTool( createRequest({ input: { accessToken: '', documentId: '' } }) diff --git a/apps/sim/lib/internal/microsoft-word/execute-tool.ts b/apps/sim/lib/internal/microsoft-word/execute-tool.ts index 9ea1b0ea7cf..b4948fc0c9c 100644 --- a/apps/sim/lib/internal/microsoft-word/execute-tool.ts +++ b/apps/sim/lib/internal/microsoft-word/execute-tool.ts @@ -24,7 +24,11 @@ import { microsoftWordReplaceTextInputSchema, microsoftWordUpdateInputSchema, } from '@/lib/internal/microsoft-word/schema' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' const logger = createLogger('MicrosoftWordToolExecution') @@ -34,7 +38,7 @@ async function executeOperation( execute: (input: z.output, context: MicrosoftWordOperationContext) => Promise, context: MicrosoftWordOperationContext, toolId: string -): Promise { +): Promise { context.signal?.throwIfAborted() let serializedInput: string try { @@ -61,7 +65,7 @@ async function executeOperation( try { const result = await execute(parsed.data, context) context.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { context.signal?.throwIfAborted() const message = getErrorMessage(error, 'Unknown error occurred') @@ -81,7 +85,9 @@ async function executeOperation( } } -export const executeMicrosoftWordTool: InternalToolOperationHandler = async (request) => { +export const executeMicrosoftWordTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { const { input, requestId, signal, toolId } = request const context: MicrosoftWordOperationContext = { requestId, signal } diff --git a/apps/sim/lib/internal/microsoft-word/operations.test.ts b/apps/sim/lib/internal/microsoft-word/operations.test.ts index 7fcc9bb4170..64add99b95d 100644 --- a/apps/sim/lib/internal/microsoft-word/operations.test.ts +++ b/apps/sim/lib/internal/microsoft-word/operations.test.ts @@ -3,6 +3,7 @@ */ import { createExecutionContext, inputValidationMock, inputValidationMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { executeMicrosoftWordExportPdf } from '@/lib/internal/microsoft-word/operations' vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) @@ -93,7 +94,7 @@ beforeEach(() => { }) }) -function executeTool(toolId: string, input: unknown): Promise { +async function executeTool(toolId: string, input: unknown): Promise { const request: InternalToolOperationCall = { toolId, input, @@ -101,13 +102,44 @@ function executeTool(toolId: string, input: unknown): Promise { context: createExecutionContext({ workflowId: 'workflow-1' }), requestId: 'request-1', } - return executeMicrosoftWordTool(request) + const result = await executeMicrosoftWordTool(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result } function executeAppend(input: typeof baseBody): Promise { return executeTool('microsoft_word_append', input) } +describe('Microsoft Word PDF file output', () => { + it('keeps large PDFs in process for the executor to store', async () => { + const buffer = Buffer.alloc(12 * 1024 * 1024, 1) + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(itemResponse('version-1')) + .mockResolvedValueOnce(new Response(buffer)) + const result = await executeMicrosoftWordExportPdf( + { accessToken: 'token-123', documentId: 'doc-abc' }, + { requestId: 'request-1' } + ) + expect(result.files).toHaveLength(1) + expect(result.files[0]?.name).toBe('notes.pdf') + expect(result.files[0]?.mimeType).toBe('application/pdf') + expect(result.files[0]?.buffer.length).toBe(buffer.length) + expect(result.files[0]?.buffer.equals(buffer)).toBe(true) + const file = { + id: 'stored', + name: 'notes.pdf', + size: buffer.length, + type: 'application/pdf', + mimeType: 'application/pdf', + url: '/api/files/stored', + key: 'execution/notes.pdf', + context: 'execution' as const, + } + expect(result.present([file])).toEqual({ success: true, output: { file } }) + }) +}) + describe('Microsoft Word direct input validation', () => { it('rejects a whitespace-only document name before provider work', async () => { const response = await executeTool('microsoft_word_create', { diff --git a/apps/sim/lib/internal/microsoft-word/operations.ts b/apps/sim/lib/internal/microsoft-word/operations.ts index 4a3d990bf45..82999611f04 100644 --- a/apps/sim/lib/internal/microsoft-word/operations.ts +++ b/apps/sim/lib/internal/microsoft-word/operations.ts @@ -17,6 +17,7 @@ import type { MicrosoftWordReplaceTextInput, MicrosoftWordUpdateInput, } from '@/lib/internal/microsoft-word/schema' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import { appendParagraphsToDocx, buildDocxFromContent, @@ -322,15 +323,8 @@ export async function executeMicrosoftWordExportPdf( name, size: pdfBuffer.length, }) - return { - success: true as const, - output: { - file: { - name, - mimeType: PDF_MIME_TYPE, - data: pdfBuffer.toString('base64'), - size: pdfBuffer.length, - }, - }, - } + return createInternalToolFileResult( + { buffer: pdfBuffer, name, mimeType: PDF_MIME_TYPE }, + (file) => ({ success: true, output: { file } }) + ) } diff --git a/apps/sim/lib/internal/onedrive/execute-tool.test.ts b/apps/sim/lib/internal/onedrive/execute-tool.test.ts index ae324b007b6..4644ef711aa 100644 --- a/apps/sim/lib/internal/onedrive/execute-tool.test.ts +++ b/apps/sim/lib/internal/onedrive/execute-tool.test.ts @@ -1,8 +1,10 @@ /** * @vitest-environment node */ + import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ downloadOneDriveFile: vi.fn(), @@ -17,10 +19,15 @@ vi.mock('@/lib/internal/onedrive/operations', () => ({ import { executeOneDriveTool } from '@/lib/internal/onedrive/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' +const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.pdf', mimeType: 'application/pdf' }, + (file) => ({ success: true, output: { file } }) +) + describe('executeOneDriveTool', () => { beforeEach(() => { vi.clearAllMocks() - mocks.downloadOneDriveFile.mockResolvedValue({ success: true, output: {} }) + mocks.downloadOneDriveFile.mockResolvedValue(fileResult) mocks.uploadOneDriveFile.mockResolvedValue({ success: true, output: {} }) }) @@ -35,7 +42,7 @@ describe('executeOneDriveTool', () => { signal: controller.signal, } - expect((await executeOneDriveTool(request)).status).toBe(200) + expect(await executeOneDriveTool(request)).toBe(fileResult) expect(mocks.downloadOneDriveFile).toHaveBeenCalledWith( { accessToken: 'token', fileId: 'file-1', fileName: undefined }, { signal: controller.signal } diff --git a/apps/sim/lib/internal/onedrive/execute-tool.ts b/apps/sim/lib/internal/onedrive/execute-tool.ts index a4791fd31d5..12bc5ade032 100644 --- a/apps/sim/lib/internal/onedrive/execute-tool.ts +++ b/apps/sim/lib/internal/onedrive/execute-tool.ts @@ -6,7 +6,10 @@ import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { OneDriveOperationError } from '@/lib/internal/onedrive/errors' import { downloadOneDriveFile, uploadOneDriveFile } from '@/lib/internal/onedrive/operations' import { oneDriveUploadInputSchema } from '@/lib/internal/onedrive/schema' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' const downloadInputSchema = z.object({ accessToken: z.string().min(1, 'Access token is required'), @@ -30,7 +33,9 @@ function inputSizeError(input: unknown): Response | null { ) } -export const executeOneDriveTool: InternalToolOperationHandler = async (request) => { +export const executeOneDriveTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() const sizeError = inputSizeError(request.input) if (sizeError) return sizeError @@ -39,11 +44,9 @@ export const executeOneDriveTool: InternalToolOperationHandler = async (request) case 'onedrive_download': { const parsed = downloadInputSchema.safeParse(request.input) if (!parsed.success) return validationErrorResponse(parsed.error) - return Response.json( - await downloadOneDriveFile( - { ...parsed.data, fileName: parsed.data.fileName ?? undefined }, - { signal: request.signal } - ) + return await downloadOneDriveFile( + { ...parsed.data, fileName: parsed.data.fileName ?? undefined }, + { signal: request.signal } ) } case 'onedrive_upload': { diff --git a/apps/sim/lib/internal/onedrive/operations.test.ts b/apps/sim/lib/internal/onedrive/operations.test.ts index 975cd038361..db46c90a502 100644 --- a/apps/sim/lib/internal/onedrive/operations.test.ts +++ b/apps/sim/lib/internal/onedrive/operations.test.ts @@ -1,7 +1,12 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { assert, beforeEach, describe, expect, it, vi } from 'vitest' +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ assertToolFileAccess: vi.fn(), @@ -63,12 +68,40 @@ describe('downloadOneDriveFile', () => { expect(mocks.secureFetchWithPinnedIP.mock.calls[1][2]).toEqual( expect.objectContaining({ signal: controller.signal }) ) - expect(result.output.file).toEqual({ + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { + name: 'report.pdf', + mimeType: 'application/pdf', + buffer: Buffer.from([1, 2, 3]), + }, + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', name: 'report.pdf', + type: 'application/pdf', mimeType: 'application/pdf', - data: 'AQID', size: 3, - }) + context: 'execution', + } + expect(result.present([storedFile])).toEqual({ success: true, output: { file: storedFile } }) + }) + + it('preserves downloads above the JSON response limit as bytes', async () => { + const buffer = Buffer.alloc(11 * 1024 * 1024, 1) + mocks.secureFetchWithPinnedIP.mockReset() + mocks.secureFetchWithPinnedIP + .mockResolvedValueOnce( + Response.json({ name: 'large.xlsx', file: { mimeType: 'application/vnd.ms-excel' } }) + ) + .mockResolvedValueOnce(new Response(buffer)) + + const result = await downloadOneDriveFile({ accessToken: 'token', fileId: 'large-file' }, {}) + + expect(result.files[0]?.buffer.equals(buffer)).toBe(true) + expect(result.files[0]).not.toHaveProperty('data') }) it('uploads plain content without an HTTP route hop and preserves text-file behavior', async () => { diff --git a/apps/sim/lib/internal/onedrive/operations.ts b/apps/sim/lib/internal/onedrive/operations.ts index c28f6002ab6..f1d5db4bf19 100644 --- a/apps/sim/lib/internal/onedrive/operations.ts +++ b/apps/sim/lib/internal/onedrive/operations.ts @@ -17,6 +17,10 @@ import { } from '@/lib/core/utils/stream-limits' import { OneDriveOperationError } from '@/lib/internal/onedrive/errors' import type { OneDriveUploadInput } from '@/lib/internal/onedrive/schema' +import { + createInternalToolFileResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import { docNotReadyMessage, isDocNotReadyError } from '@/lib/uploads/utils/doc-not-ready' import { getExtensionFromMimeType, @@ -25,7 +29,7 @@ import { import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' import { assertToolFileAccess } from '@/app/api/files/authorization' -import type { OneDriveDownloadResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { OneDriveToolParams } from '@/tools/onedrive/types' import { normalizeExcelValues } from '@/tools/onedrive/utils' const MAX_GRAPH_JSON_BYTES = 2 * 1024 * 1024 @@ -452,7 +456,7 @@ async function graphError(response: SecureFetchResponse, fallback: string, signa export async function downloadOneDriveFile( input: OneDriveDownloadInput, context: OneDriveOperationContext -): Promise { +): Promise { context.signal?.throwIfAborted() const fileId = encodeURIComponent(input.fileId) const metadataResponse = await fetchGraph( @@ -498,15 +502,12 @@ export async function downloadOneDriveFile( label: 'OneDrive file download', signal: context.signal, }) - return { - success: true, - output: { - file: { - name: input.fileName || metadata.name || 'download', - mimeType: metadata.file?.mimeType || 'application/octet-stream', - data: buffer.toString('base64'), - size: buffer.length, - }, + return createInternalToolFileResult( + { + buffer, + name: input.fileName || metadata.name || 'download', + mimeType: metadata.file?.mimeType || 'application/octet-stream', }, - } + (file) => ({ success: true, output: { file } }) + ) } diff --git a/apps/sim/lib/internal/onepassword/execute-tool.ts b/apps/sim/lib/internal/onepassword/execute-tool.ts index 47aec72789c..4b8144709a1 100644 --- a/apps/sim/lib/internal/onepassword/execute-tool.ts +++ b/apps/sim/lib/internal/onepassword/execute-tool.ts @@ -27,10 +27,12 @@ import { executeOnePasswordUpdateItem, type OnePasswordOperationContext, } from '@/lib/internal/onepassword/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import { parseInternalToolInput } from '@/lib/internal/tool-operations/parse-input' import type { InternalToolOperationCall, InternalToolOperationHandler, + InternalToolOperationResult, } from '@/lib/internal/tool-operations/types' const logger = createLogger('OnePasswordToolExecution') @@ -40,7 +42,7 @@ async function executeOperation( request: InternalToolOperationCall, operation: (input: ContractBody, context: OnePasswordOperationContext) => Promise, failureMessage: string -): Promise { +): Promise { request.signal?.throwIfAborted() const parsed = parseInternalToolInput(contract, request.input) if (!parsed.success) return parsed.response @@ -48,7 +50,7 @@ async function executeOperation( try { const result = await operation(parsed.data, { signal: request.signal }) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof OnePasswordOperationError) { @@ -64,7 +66,9 @@ async function executeOperation( } } -export const executeOnePasswordTool: InternalToolOperationHandler = async (request) => { +export const executeOnePasswordTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { switch (request.toolId) { case 'onepassword_list_vaults': return executeOperation( diff --git a/apps/sim/lib/internal/onepassword/operations.test.ts b/apps/sim/lib/internal/onepassword/operations.test.ts index 76d7518f514..aafdf3f4dc9 100644 --- a/apps/sim/lib/internal/onepassword/operations.test.ts +++ b/apps/sim/lib/internal/onepassword/operations.test.ts @@ -1,7 +1,12 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { assert, beforeEach, describe, expect, it, vi } from 'vitest' +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' const clientMocks = vi.hoisted(() => ({ connectItemToSdkItem: vi.fn(), @@ -201,20 +206,65 @@ describe('1Password operations', () => { { signal: controller.signal } ) - expect(result).toEqual({ - file: { + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { name: 'secret.txt', mimeType: 'text/plain', - data: Buffer.from('hello').toString('base64'), - size: 5, + buffer: Buffer.from('hello'), }, - }) + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', + name: 'secret.txt', + type: 'text/plain', + mimeType: 'text/plain', + size: 5, + context: 'execution', + } + expect(result.present([storedFile])).toEqual({ file: storedFile }) expect(clientMocks.connectRequest.mock.calls[1]?.[0]).toMatchObject({ maxResponseBytes: 5, signal: controller.signal, }) }) + it('returns SDK attachment bytes for storage with the actual byte length', async () => { + clientMocks.createOnePasswordClient.mockResolvedValue({ + items: { + get: vi.fn().mockResolvedValue({ id: 'item-1' }), + files: { read: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])) }, + }, + }) + clientMocks.findItemFileAttributes.mockReturnValue({ + id: 'file-1', + name: 'secret.bin', + size: 3, + }) + + const result = await executeOnePasswordGetItemFile( + { ...SERVICE_CREDENTIALS, vaultId: 'vault-1', itemId: 'item-1', fileId: 'file-1' }, + {} + ) + + expect(result.files).toEqual([ + { name: 'secret.bin', mimeType: 'application/octet-stream', buffer: Buffer.from([1, 2, 3]) }, + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', + name: 'secret.bin', + type: 'application/octet-stream', + mimeType: 'application/octet-stream', + size: 3, + context: 'execution', + } + expect(result.present([storedFile])).toEqual({ file: storedFile }) + }) + it('preserves the private secret value and rejects Connect mode', async () => { const resolve = vi.fn().mockResolvedValue('resolved-secret') clientMocks.createOnePasswordClient.mockResolvedValue({ secrets: { resolve } }) diff --git a/apps/sim/lib/internal/onepassword/operations.ts b/apps/sim/lib/internal/onepassword/operations.ts index 27721610f4b..d33b3ba1cb0 100644 --- a/apps/sim/lib/internal/onepassword/operations.ts +++ b/apps/sim/lib/internal/onepassword/operations.ts @@ -32,6 +32,10 @@ import { applyOnePasswordPatch, type JsonPatchOperation, } from '@/lib/internal/onepassword/json-patch' +import { + createInternalToolFileResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' export interface OnePasswordOperationContext { @@ -373,9 +377,7 @@ export async function executeOnePasswordResolveSecret( export async function executeOnePasswordGetItemFile( input: GetItemFileInput, context: OnePasswordOperationContext -): Promise<{ - file: { name: string; mimeType: string; data: string; size: number } -}> { +): Promise { const credentials = resolveCredentials(input) if (credentials.mode === 'service_account') { const client = await createOnePasswordClient(credentials.serviceAccountToken, context.signal) @@ -390,14 +392,10 @@ export async function executeOnePasswordGetItemFile( ) assertKnownSizeWithinLimit(content.byteLength, MAX_FILE_SIZE, '1Password item file') const buffer = Buffer.from(content.buffer, content.byteOffset, content.byteLength) - return { - file: { - name: attributes.name, - mimeType: 'application/octet-stream', - data: buffer.toString('base64'), - size: attributes.size, - }, - } + return createInternalToolFileResult( + { buffer, name: attributes.name, mimeType: 'application/octet-stream' }, + (file) => ({ file }) + ) } const metadataResponse = await connectRequest({ @@ -434,12 +432,12 @@ export async function executeOnePasswordGetItemFile( } const buffer = Buffer.from(await contentResponse.arrayBuffer()) context.signal?.throwIfAborted() - return { - file: { + return createInternalToolFileResult( + { + buffer, name: typeof metadata.name === 'string' ? metadata.name : 'attachment', mimeType: contentResponse.headers.get('content-type') || 'application/octet-stream', - data: buffer.toString('base64'), - size: typeof metadata.size === 'number' ? metadata.size : buffer.length, }, - } + (file) => ({ file }) + ) } diff --git a/apps/sim/lib/internal/outlook/client.test.ts b/apps/sim/lib/internal/outlook/client.test.ts index 1954a636768..c085bca9b04 100644 --- a/apps/sim/lib/internal/outlook/client.test.ts +++ b/apps/sim/lib/internal/outlook/client.test.ts @@ -2,7 +2,7 @@ * @vitest-environment node */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { DEFAULT_MAX_ERROR_BODY_BYTES, PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { OutlookClient } from '@/lib/internal/outlook/client' import { OutlookOperationError } from '@/lib/internal/outlook/errors' @@ -86,6 +86,94 @@ describe('OutlookClient', () => { ) }) + it('reads raw attachments larger than 10 MiB with OAuth and cancellation', async () => { + const bytes = Buffer.alloc(12 * 1024 * 1024, 4) + fetchMock.mockResolvedValue( + new Response(bytes, { headers: { 'content-type': 'application/pdf' } }) + ) + const controller = new AbortController() + + const result = await new OutlookClient('access-token').buffer( + '/me/messages/message-1/attachments/file-1/$value', + 100 * 1024 * 1024, + 'Failed to download attachment', + controller.signal + ) + + expect(result.buffer.equals(bytes)).toBe(true) + expect(result.contentType).toBe('application/pdf') + expect(fetchMock).toHaveBeenCalledWith( + 'https://graph.microsoft.com/v1.0/me/messages/message-1/attachments/file-1/$value', + { + method: 'GET', + headers: { Authorization: 'Bearer access-token' }, + signal: controller.signal, + } + ) + }) + + it('accepts a zero-byte attachment body', async () => { + fetchMock.mockResolvedValue(new Response(new Uint8Array(0))) + + const result = await new OutlookClient('access-token').buffer( + '/attachment/$value', + 1024, + 'Failed' + ) + + expect(result.buffer.byteLength).toBe(0) + }) + + it.each(['declared', 'actual'])('enforces the %s raw-body limit', async (sizeSource) => { + fetchMock.mockResolvedValue( + new Response(new Uint8Array(1025), { + headers: { 'content-length': sizeSource === 'declared' ? '1025' : '1' }, + }) + ) + + await expect( + new OutlookClient('access-token').buffer('/attachment/$value', 1024, 'Failed') + ).rejects.toBeInstanceOf(PayloadSizeLimitError) + }) + + it('preserves raw-download Graph error messages and status', async () => { + fetchMock.mockResolvedValue( + Response.json({ error: { message: 'Access denied' } }, { status: 403 }) + ) + + await expect( + new OutlookClient('access-token').buffer('/attachment/$value', 1024, 'Failed to download') + ).rejects.toEqual(new OutlookOperationError('Access denied', 403)) + }) + + it('bounds raw-download error bodies while preserving provider status', async () => { + fetchMock.mockResolvedValue( + new Response('bad gateway', { + status: 502, + headers: { 'content-length': String(DEFAULT_MAX_ERROR_BODY_BYTES + 1) }, + }) + ) + + await expect( + new OutlookClient('access-token').buffer('/attachment/$value', 1024, 'Failed to download') + ).rejects.toEqual(new OutlookOperationError('Failed to download', 502)) + }) + + it('rejects cancelled raw downloads before fetch', async () => { + const controller = new AbortController() + controller.abort(new DOMException('cancelled', 'AbortError')) + + await expect( + new OutlookClient('access-token').buffer( + '/attachment/$value', + 1024, + 'Failed', + controller.signal + ) + ).rejects.toMatchObject({ name: 'AbortError' }) + expect(fetchMock).not.toHaveBeenCalled() + }) + it('stops before provider work when already cancelled', async () => { const controller = new AbortController() controller.abort(new DOMException('cancelled', 'AbortError')) diff --git a/apps/sim/lib/internal/outlook/client.ts b/apps/sim/lib/internal/outlook/client.ts index 7c4e70035bb..2b7a2ac7e2b 100644 --- a/apps/sim/lib/internal/outlook/client.ts +++ b/apps/sim/lib/internal/outlook/client.ts @@ -1,5 +1,9 @@ import { getErrorMessage } from '@sim/utils/errors' -import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits' +import { + DEFAULT_MAX_ERROR_BODY_BYTES, + readResponseTextWithLimit, + readResponseToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' import { OutlookOperationError } from '@/lib/internal/outlook/errors' const MICROSOFT_GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0' @@ -98,4 +102,40 @@ export class OutlookClient { await response.body?.cancel() signal?.throwIfAborted() } + + async buffer( + path: string, + maxBytes: number, + fallbackError: string, + signal?: AbortSignal + ): Promise<{ buffer: Buffer; contentType: string | null }> { + signal?.throwIfAborted() + const response = await fetch(this.url(path), { + method: 'GET', + headers: { Authorization: `Bearer ${this.accessToken}` }, + signal, + }) + if (!response.ok) { + let data: OutlookJsonObject = {} + try { + data = parseJson( + await readResponseTextWithLimit(response, { + maxBytes: DEFAULT_MAX_ERROR_BODY_BYTES, + label: 'Microsoft Graph error response', + signal, + }) + ) + } catch { + signal?.throwIfAborted() + } + throw new OutlookOperationError(graphErrorMessage(data, fallbackError), response.status) + } + const buffer = await readResponseToBufferWithLimit(response, { + maxBytes, + label: 'Outlook attachment', + signal, + }) + signal?.throwIfAborted() + return { buffer, contentType: response.headers.get('content-type') } + } } diff --git a/apps/sim/lib/internal/outlook/execute-tool.test.ts b/apps/sim/lib/internal/outlook/execute-tool.test.ts index 63a032b41b4..cdeb57e9030 100644 --- a/apps/sim/lib/internal/outlook/execute-tool.test.ts +++ b/apps/sim/lib/internal/outlook/execute-tool.test.ts @@ -8,6 +8,7 @@ const operationMocks = vi.hoisted(() => ({ executeOutlookCopy: vi.fn(), executeOutlookDelete: vi.fn(), executeOutlookDraft: vi.fn(), + executeOutlookGetAttachment: vi.fn(), executeOutlookMarkRead: vi.fn(), executeOutlookMarkUnread: vi.fn(), executeOutlookMove: vi.fn(), @@ -18,9 +19,16 @@ vi.mock('@/lib/internal/outlook/operations', () => operationMocks) import { DEFAULT_MAX_JSON_BODY_BYTES } from '@/lib/api/server/validation' import { OutlookOperationError } from '@/lib/internal/outlook/errors' -import { executeOutlookTool } from '@/lib/internal/outlook/execute-tool' +import { executeOutlookTool as executeOutlookToolOperation } from '@/lib/internal/outlook/execute-tool' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' +async function executeOutlookTool(request: InternalToolOperationCall): Promise { + const result = await executeOutlookToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + const MESSAGE_BODY = { accessToken: 'access-token', messageId: 'message-1' } const COPY_MOVE_BODY = { ...MESSAGE_BODY, destinationId: 'folder-1' } const MAIL_BODY = { @@ -51,6 +59,12 @@ const TOOL_CASES = [ ['outlook_copy', COPY_MOVE_BODY, operationMocks.executeOutlookCopy, 'provider'], ['outlook_delete', MESSAGE_BODY, operationMocks.executeOutlookDelete, 'provider'], ['outlook_draft', MAIL_BODY, operationMocks.executeOutlookDraft, 'mail'], + [ + 'outlook_get_attachment', + { ...MESSAGE_BODY, attachmentId: 'attachment-1' }, + operationMocks.executeOutlookGetAttachment, + 'provider', + ], ['outlook_mark_read', MESSAGE_BODY, operationMocks.executeOutlookMarkRead, 'provider'], ['outlook_mark_unread', MESSAGE_BODY, operationMocks.executeOutlookMarkUnread, 'provider'], ['outlook_move', COPY_MOVE_BODY, operationMocks.executeOutlookMove, 'provider'], @@ -86,6 +100,55 @@ describe('executeOutlookTool', () => { } ) + it('passes large attachment bytes to the central presenter without serializing them', async () => { + const buffer = Buffer.alloc(12 * 1024 * 1024) + const fileResult = createInternalToolFileResult( + { buffer, name: 'report.xlsx', mimeType: 'application/octet-stream' }, + (file) => ({ success: true, output: { attachments: [file] } }) + ) + operationMocks.executeOutlookGetAttachment.mockResolvedValue(fileResult) + + const result = await executeOutlookToolOperation( + createRequest({ + toolId: 'outlook_get_attachment', + input: { ...MESSAGE_BODY, attachmentId: 'attachment-1' }, + }) + ) + + expect(result).toBe(fileResult) + }) + + it('rejects invalid attachment IDs before provider work', async () => { + const response = await executeOutlookTool( + createRequest({ + toolId: 'outlook_get_attachment', + input: { ...MESSAGE_BODY, attachmentId: ' ' }, + }) + ) + + expect(response.status).toBe(400) + expect(operationMocks.executeOutlookGetAttachment).not.toHaveBeenCalled() + }) + + it('preserves attachment size errors as 413 responses', async () => { + operationMocks.executeOutlookGetAttachment.mockRejectedValue( + new OutlookOperationError('Outlook attachment exceeds the size limit', 413) + ) + + const response = await executeOutlookTool( + createRequest({ + toolId: 'outlook_get_attachment', + input: { ...MESSAGE_BODY, attachmentId: 'attachment-1' }, + }) + ) + + expect(response.status).toBe(413) + await expect(response.json()).resolves.toEqual({ + success: false, + error: 'Outlook attachment exceeds the size limit', + }) + }) + it('returns the canonical validation envelope before provider work', async () => { const response = await executeOutlookTool( createRequest({ input: { accessToken: '', messageId: '' } }) diff --git a/apps/sim/lib/internal/outlook/execute-tool.ts b/apps/sim/lib/internal/outlook/execute-tool.ts index b209433362c..6c219f184e4 100644 --- a/apps/sim/lib/internal/outlook/execute-tool.ts +++ b/apps/sim/lib/internal/outlook/execute-tool.ts @@ -11,34 +11,48 @@ import { } from '@/lib/api/contracts/tools/microsoft' import { DEFAULT_MAX_JSON_BODY_BYTES } from '@/lib/api/server/validation' import { OutlookOperationError } from '@/lib/internal/outlook/errors' +import { outlookGetAttachmentInputSchema } from '@/lib/internal/outlook/get-attachment-input' import { executeOutlookCopy, executeOutlookDelete, executeOutlookDraft, + executeOutlookGetAttachment, executeOutlookMarkRead, executeOutlookMarkUnread, executeOutlookMove, executeOutlookSend, type OutlookMailOperationContext, } from '@/lib/internal/outlook/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import { parseInternalOperationInput } from '@/lib/internal/tool-operations/parse-contract-input' import { parseInternalToolInput } from '@/lib/internal/tool-operations/parse-input' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' async function executeOperation( contract: C, input: unknown, execute: (input: ContractBody) => Promise, signal?: AbortSignal -): Promise { +): Promise { signal?.throwIfAborted() const parsed = parseInternalToolInput(contract, input, { maxInputBytes: DEFAULT_MAX_JSON_BODY_BYTES, }) if (!parsed.success) return parsed.response + return executeAndPresent(() => execute(parsed.data), signal) +} + +async function executeAndPresent( + execute: () => Promise, + signal?: AbortSignal +): Promise { try { - const result = await execute(parsed.data) + const result = await execute() signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { signal?.throwIfAborted() if (error instanceof OutlookOperationError) { @@ -51,14 +65,24 @@ async function executeOperation( } } -export const executeOutlookTool: InternalToolOperationHandler = async (request) => { +export const executeOutlookTool: InternalToolOperationHandler = async ( + request +) => { const { input, context, requestId, signal, toolId } = request + signal?.throwIfAborted() const mailContext: OutlookMailOperationContext = { requestId, signal, userId: context.userId, } switch (toolId) { + case 'outlook_get_attachment': { + const parsed = parseInternalOperationInput({ body: outlookGetAttachmentInputSchema }, input, { + maxInputBytes: DEFAULT_MAX_JSON_BODY_BYTES, + }) + if (!parsed.success) return parsed.response + return executeAndPresent(() => executeOutlookGetAttachment(parsed.data.body, signal), signal) + } case 'outlook_copy': return executeOperation( outlookCopyContract, diff --git a/apps/sim/lib/internal/outlook/get-attachment-input.ts b/apps/sim/lib/internal/outlook/get-attachment-input.ts new file mode 100644 index 00000000000..8e423a4a6d7 --- /dev/null +++ b/apps/sim/lib/internal/outlook/get-attachment-input.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' +import { accessTokenSchema, messageIdSchema } from '@/lib/api/contracts/tools/microsoft' + +export const outlookGetAttachmentInputSchema = z.object({ + accessToken: accessTokenSchema, + messageId: messageIdSchema.trim().min(1, 'Message ID is required'), + attachmentId: z.string().trim().min(1, 'Attachment ID is required'), +}) + +export type OutlookGetAttachmentInput = z.infer diff --git a/apps/sim/lib/internal/outlook/operations.test.ts b/apps/sim/lib/internal/outlook/operations.test.ts index 2587dc1acca..e5d39b3fbc8 100644 --- a/apps/sim/lib/internal/outlook/operations.test.ts +++ b/apps/sim/lib/internal/outlook/operations.test.ts @@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({ assertToolFileAccess: vi.fn(), downloadServableFilesWithinBudget: vi.fn(), empty: vi.fn(), + buffer: vi.fn(), json: vi.fn(), processFilesToUserFiles: vi.fn(), })) @@ -17,6 +18,10 @@ vi.mock('@/lib/internal/outlook/client', () => ({ return mocks.json(...args) } + buffer(...args: unknown[]) { + return mocks.buffer(...args) + } + empty(...args: unknown[]) { return mocks.empty(...args) } @@ -38,11 +43,17 @@ import { executeOutlookCopy, executeOutlookDelete, executeOutlookDraft, + executeOutlookGetAttachment, executeOutlookMarkRead, executeOutlookMarkUnread, executeOutlookMove, executeOutlookSend, } from '@/lib/internal/outlook/operations' +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' const MAIL_INPUT = { accessToken: 'access-token', @@ -81,6 +92,7 @@ describe('Outlook operations', () => { { buffer: Buffer.from('report'), contentType: 'application/pdf' }, ]) mocks.empty.mockResolvedValue(undefined) + mocks.buffer.mockResolvedValue({ buffer: Buffer.alloc(0), contentType: null }) mocks.json.mockResolvedValue({}) mocks.processFilesToUserFiles.mockReturnValue([]) }) @@ -333,3 +345,182 @@ describe('Outlook operations', () => { expect(mocks.assertToolFileAccess).not.toHaveBeenCalled() }) }) + +const ATTACHMENT_INPUT = { + accessToken: 'access-token', + messageId: ' message/1 ', + attachmentId: ' attachment/1 ', +} + +const ATTACHMENT_METADATA = { + '@odata.type': '#microsoft.graph.fileAttachment', + id: 'attachment/1', + name: 'report.xlsx', + contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + size: 12 * 1024 * 1024, + isInline: false, + lastModifiedDateTime: '2026-09-11T10:00:00Z', +} + +describe('Outlook attachment downloads', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.json.mockResolvedValue(ATTACHMENT_METADATA) + mocks.buffer.mockResolvedValue({ buffer: Buffer.alloc(0), contentType: null }) + }) + + it('fetches metadata separately and presents a large file as a stored reference', async () => { + const buffer = Buffer.alloc(12 * 1024 * 1024, 1) + const controller = new AbortController() + mocks.buffer.mockResolvedValue({ buffer, contentType: 'application/octet-stream' }) + + const result = await executeOutlookGetAttachment(ATTACHMENT_INPUT, controller.signal) + + expect(mocks.json).toHaveBeenCalledWith( + '/me/messages/message%2F1/attachments/attachment%2F1?$select=id,name,contentType,size,isInline,lastModifiedDateTime', + { method: 'GET' }, + 'Failed to retrieve attachment', + controller.signal + ) + expect(mocks.buffer).toHaveBeenCalledWith( + '/me/messages/message%2F1/attachments/attachment%2F1/$value', + MAX_BUFFERED_TRANSFER_BYTES, + 'Failed to download attachment', + controller.signal + ) + if (!isInternalToolFileResult(result)) throw new Error('Expected a file result') + expect(result.files).toHaveLength(1) + expect(result.files[0]?.buffer).toBe(buffer) + expect(result.files[0]?.name).toBe(ATTACHMENT_METADATA.name) + expect(result.files[0]?.mimeType).toBe(ATTACHMENT_METADATA.contentType) + const stored: StoredToolFile = { + id: 'stored-1', + key: 'execution/stored-1', + name: ATTACHMENT_METADATA.name, + size: buffer.byteLength, + type: ATTACHMENT_METADATA.contentType, + mimeType: ATTACHMENT_METADATA.contentType, + url: '/api/files/serve/stored-1', + } + const body = result.present([stored]) + expect(body).toEqual({ + success: true, + output: { + message: 'Successfully retrieved attachment "report.xlsx".', + results: { + id: 'attachment/1', + name: ATTACHMENT_METADATA.name, + contentType: ATTACHMENT_METADATA.contentType, + size: buffer.byteLength, + isInline: false, + attachmentType: '#microsoft.graph.fileAttachment', + lastModifiedDateTime: ATTACHMENT_METADATA.lastModifiedDateTime, + }, + attachments: [stored], + }, + }) + expect(Buffer.byteLength(JSON.stringify(body))).toBeLessThan(2000) + expect(JSON.stringify(body)).not.toContain('contentBytes') + }) + + it('preserves zero-byte file attachments', async () => { + mocks.json.mockResolvedValue({ ...ATTACHMENT_METADATA, size: 0 }) + + const result = await executeOutlookGetAttachment(ATTACHMENT_INPUT) + + if (!isInternalToolFileResult(result)) throw new Error('Expected a file result') + expect(result.files[0]?.buffer.byteLength).toBe(0) + expect(mocks.buffer).toHaveBeenCalledOnce() + }) + + it.each(['#microsoft.graph.itemAttachment', '#microsoft.graph.referenceAttachment'])( + 'preserves %s metadata without trying to download raw content', + async (attachmentType) => { + mocks.json.mockResolvedValue({ ...ATTACHMENT_METADATA, '@odata.type': attachmentType }) + + const result = await executeOutlookGetAttachment(ATTACHMENT_INPUT) + + expect(result).toMatchObject({ + success: true, + output: { + results: { attachmentType, name: 'report.xlsx' }, + attachments: [], + }, + }) + expect(mocks.buffer).not.toHaveBeenCalled() + } + ) + + it('rejects metadata above 100 MiB before fetching file bytes', async () => { + mocks.json.mockResolvedValue({ + ...ATTACHMENT_METADATA, + size: MAX_BUFFERED_TRANSFER_BYTES + 1, + }) + + await expect(executeOutlookGetAttachment(ATTACHMENT_INPUT)).rejects.toMatchObject({ + status: 413, + }) + expect(mocks.buffer).not.toHaveBeenCalled() + }) + + it('accepts the exact metadata size limit and bounds the raw body independently', async () => { + mocks.json.mockResolvedValue({ ...ATTACHMENT_METADATA, size: MAX_BUFFERED_TRANSFER_BYTES }) + mocks.buffer.mockRejectedValue( + new PayloadSizeLimitError({ + label: 'Outlook attachment', + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + observedBytes: MAX_BUFFERED_TRANSFER_BYTES + 1, + }) + ) + + await expect(executeOutlookGetAttachment(ATTACHMENT_INPUT)).rejects.toMatchObject({ + status: 413, + }) + expect(mocks.buffer).toHaveBeenCalledOnce() + }) + + it.each(['metadata', 'download'])('preserves Graph %s errors and HTTP status', async (phase) => { + const error = new OutlookOperationError('Attachment not found', 404) + if (phase === 'metadata') mocks.json.mockRejectedValueOnce(error) + else mocks.buffer.mockRejectedValueOnce(error) + + await expect(executeOutlookGetAttachment(ATTACHMENT_INPUT)).rejects.toBe(error) + if (phase === 'metadata') expect(mocks.buffer).not.toHaveBeenCalled() + }) + + it('uses response MIME and a fallback filename when metadata omits them', async () => { + mocks.json.mockResolvedValue({ '@odata.type': '#microsoft.graph.fileAttachment' }) + mocks.buffer.mockResolvedValue({ buffer: Buffer.from('text'), contentType: 'text/plain' }) + + const result = await executeOutlookGetAttachment(ATTACHMENT_INPUT) + + if (!isInternalToolFileResult(result)) throw new Error('Expected a file result') + expect(result.files[0]?.name).toBe('attachment') + expect(result.files[0]?.mimeType).toBe('text/plain') + }) + + it('stops between metadata and raw downloads when cancelled', async () => { + const controller = new AbortController() + mocks.json.mockImplementationOnce(async () => { + controller.abort(new DOMException('cancelled', 'AbortError')) + return ATTACHMENT_METADATA + }) + + await expect( + executeOutlookGetAttachment(ATTACHMENT_INPUT, controller.signal) + ).rejects.toMatchObject({ name: 'AbortError' }) + expect(mocks.buffer).not.toHaveBeenCalled() + }) + + it('does not produce a file result if cancellation occurs during download', async () => { + const controller = new AbortController() + mocks.buffer.mockImplementationOnce(async () => { + controller.abort(new DOMException('cancelled', 'AbortError')) + return { buffer: Buffer.alloc(0), contentType: null } + }) + + await expect( + executeOutlookGetAttachment(ATTACHMENT_INPUT, controller.signal) + ).rejects.toMatchObject({ name: 'AbortError' }) + }) +}) diff --git a/apps/sim/lib/internal/outlook/operations.ts b/apps/sim/lib/internal/outlook/operations.ts index 1f6594c1f0a..972e1c4cb33 100644 --- a/apps/sim/lib/internal/outlook/operations.ts +++ b/apps/sim/lib/internal/outlook/operations.ts @@ -9,17 +9,22 @@ import type { OutlookMoveBody, OutlookSendBody, } from '@/lib/api/contracts/tools/microsoft' -import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { assertKnownSizeWithinLimit, isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { OutlookClient, type OutlookJsonObject } from '@/lib/internal/outlook/client' import { OutlookOperationError } from '@/lib/internal/outlook/errors' +import type { OutlookGetAttachmentInput } from '@/lib/internal/outlook/get-attachment-input' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { docNotReadyMessage, isDocNotReadyError } from '@/lib/uploads/utils/doc-not-ready' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadServableFilesWithinBudget } from '@/lib/uploads/utils/file-utils.server' import { assertToolFileAccess } from '@/app/api/files/authorization' +import type { CleanedOutlookAttachmentMetadata } from '@/tools/outlook/types' const logger = createLogger('OutlookOperations') const OUTLOOK_SEND_ATTACHMENT_MAX_BYTES = 3 * 1024 * 1024 const OUTLOOK_DRAFT_ATTACHMENT_MAX_BYTES = 4 * 1024 * 1024 +const OUTLOOK_ATTACHMENT_METADATA_FIELDS = 'id,name,contentType,size,isInline,lastModifiedDateTime' interface OutlookMailOperationContext { requestId: string @@ -148,6 +153,62 @@ async function buildMessage( return message } +export async function executeOutlookGetAttachment( + input: OutlookGetAttachmentInput, + signal?: AbortSignal +) { + signal?.throwIfAborted() + const client = new OutlookClient(input.accessToken) + const path = `/me/messages/${encodeURIComponent(input.messageId.trim())}/attachments/${encodeURIComponent(input.attachmentId.trim())}` + try { + const data = await client.json( + `${path}?$select=${OUTLOOK_ATTACHMENT_METADATA_FIELDS}`, + { method: 'GET' }, + 'Failed to retrieve attachment', + signal + ) + signal?.throwIfAborted() + const results: CleanedOutlookAttachmentMetadata = { + id: optionalString(data, 'id') ?? input.attachmentId.trim(), + name: optionalString(data, 'name') ?? null, + contentType: optionalString(data, 'contentType') ?? null, + size: typeof data.size === 'number' ? data.size : null, + isInline: optionalBoolean(data, 'isInline') ?? null, + attachmentType: optionalString(data, '@odata.type') ?? null, + lastModifiedDateTime: optionalString(data, 'lastModifiedDateTime') ?? null, + } + const output = { + message: `Successfully retrieved attachment "${results.name ?? ''}".`, + results, + } + if (results.attachmentType !== '#microsoft.graph.fileAttachment') { + return { success: true, output: { ...output, attachments: [] } } + } + if (results.size !== null && results.size !== undefined) { + assertKnownSizeWithinLimit(results.size, MAX_BUFFERED_TRANSFER_BYTES, 'Outlook attachment') + } + const { buffer, contentType } = await client.buffer( + `${path}/$value`, + MAX_BUFFERED_TRANSFER_BYTES, + 'Failed to download attachment', + signal + ) + signal?.throwIfAborted() + return createInternalToolFileResult( + { + buffer, + name: results.name || 'attachment', + mimeType: results.contentType || contentType || 'application/octet-stream', + }, + (file) => ({ success: true, output: { ...output, attachments: [file] } }) + ) + } catch (error) { + signal?.throwIfAborted() + if (isPayloadSizeLimitError(error)) throw new OutlookOperationError(error.message, 413) + throw error + } +} + export async function executeOutlookCopy(input: OutlookCopyBody, signal?: AbortSignal) { const client = new OutlookClient(input.accessToken) const data = await client.json( diff --git a/apps/sim/lib/internal/pipedrive/execute-tool.ts b/apps/sim/lib/internal/pipedrive/execute-tool.ts index 78394ababff..ba5baf9db86 100644 --- a/apps/sim/lib/internal/pipedrive/execute-tool.ts +++ b/apps/sim/lib/internal/pipedrive/execute-tool.ts @@ -5,7 +5,11 @@ import { DEFAULT_MAX_JSON_BODY_BYTES } from '@/lib/api/server/validation' import { PipedriveOperationError } from '@/lib/internal/pipedrive/errors' import { executePipedriveGetFiles } from '@/lib/internal/pipedrive/operations' import { pipedriveGetFilesInputSchema } from '@/lib/internal/pipedrive/schema' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' const logger = createLogger('PipedriveToolExecution') @@ -26,7 +30,9 @@ function inputSizeError(input: unknown): Response | null { : null } -export const executePipedriveTool: InternalToolOperationHandler = async (request) => { +export const executePipedriveTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() if (request.toolId !== 'pipedrive_get_files') { return Response.json( @@ -52,7 +58,7 @@ export const executePipedriveTool: InternalToolOperationHandler = async (request signal: request.signal, }) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof PipedriveOperationError) { diff --git a/apps/sim/lib/internal/pipedrive/operations.test.ts b/apps/sim/lib/internal/pipedrive/operations.test.ts new file mode 100644 index 00000000000..5b686ca2863 --- /dev/null +++ b/apps/sim/lib/internal/pipedrive/operations.test.ts @@ -0,0 +1,84 @@ +/** + * @vitest-environment node + */ +import { assert, beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + downloadPipedriveFile: vi.fn(), + listPipedriveFiles: vi.fn(), +})) + +vi.mock('@/lib/internal/pipedrive/client', () => mocks) + +import { executePipedriveGetFiles } from '@/lib/internal/pipedrive/operations' +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' + +describe('executePipedriveGetFiles', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.listPipedriveFiles.mockResolvedValue({ + files: [{ id: 1, name: 'report.pdf', url: 'https://files.example/report.pdf' }], + hasMore: true, + nextStart: 1, + }) + }) + + it('keeps large downloads out of JSON while preserving file-list pagination', async () => { + const buffer = Buffer.alloc(11 * 1024 * 1024, 1) + mocks.downloadPipedriveFile.mockResolvedValue({ buffer, contentType: 'application/pdf' }) + const controller = new AbortController() + const input = { accessToken: 'token', downloadFiles: true } + const result = await executePipedriveGetFiles(input, { + requestId: 'request-1', + signal: controller.signal, + }) + + assert(isInternalToolFileResult(result)) + expect(result.files).toHaveLength(1) + expect(result.files[0]?.buffer).toBe(buffer) + expect(result.files[0]?.name).toBe('report.pdf') + expect(result.files[0]?.mimeType).toBe('application/pdf') + expect(mocks.downloadPipedriveFile).toHaveBeenCalledWith( + 'https://files.example/report.pdf', + input, + MAX_BUFFERED_TRANSFER_BYTES, + controller.signal + ) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', + name: 'report.pdf', + type: 'application/pdf', + mimeType: 'application/pdf', + size: buffer.length, + context: 'execution', + } + expect(result.present([storedFile])).toEqual({ + success: true, + output: { + files: [{ id: 1, name: 'report.pdf', url: 'https://files.example/report.pdf' }], + downloadedFiles: [storedFile], + total_items: 1, + has_more: true, + next_start: 1, + success: true, + }, + }) + }) + + it('returns metadata without file persistence when downloads are disabled', async () => { + const result = await executePipedriveGetFiles( + { accessToken: 'token', downloadFiles: false }, + { requestId: 'request-1' } + ) + + expect(isInternalToolFileResult(result)).toBe(false) + expect(result).toMatchObject({ success: true, output: { has_more: true, next_start: 1 } }) + expect(mocks.downloadPipedriveFile).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/internal/pipedrive/operations.ts b/apps/sim/lib/internal/pipedrive/operations.ts index 0418852eeca..068def9a162 100644 --- a/apps/sim/lib/internal/pipedrive/operations.ts +++ b/apps/sim/lib/internal/pipedrive/operations.ts @@ -1,6 +1,10 @@ import { createLogger } from '@sim/logger' import { downloadPipedriveFile, listPipedriveFiles } from '@/lib/internal/pipedrive/client' import type { PipedriveGetFilesInput } from '@/lib/internal/pipedrive/schema' +import { + createInternalToolFilesResult, + type InternalToolFile, +} from '@/lib/internal/tool-operations/file-result' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' @@ -17,12 +21,7 @@ export async function executePipedriveGetFiles( ) { context.signal?.throwIfAborted() const page = await listPipedriveFiles(input, context.signal) - const downloadedFiles: Array<{ - data: string - mimeType: string - name: string - size: number - }> = [] + const downloadedFiles: InternalToolFile[] = [] let downloadedBytes = 0 if (input.downloadFiles) { @@ -43,8 +42,7 @@ export async function executePipedriveGetFiles( downloadedFiles.push({ name, mimeType: downloaded.contentType || getMimeTypeFromExtension(extension), - data: downloaded.buffer.toString('base64'), - size: downloaded.buffer.length, + buffer: downloaded.buffer, }) } catch (error) { context.signal?.throwIfAborted() @@ -56,15 +54,16 @@ export async function executePipedriveGetFiles( } } context.signal?.throwIfAborted() - return { + const output = { + files: page.files, + total_items: page.files.length, + has_more: page.hasMore, + next_start: page.nextStart, success: true, - output: { - files: page.files, - downloadedFiles: downloadedFiles.length > 0 ? downloadedFiles : undefined, - total_items: page.files.length, - has_more: page.hasMore, - next_start: page.nextStart, - success: true, - }, } + if (downloadedFiles.length === 0) return { success: true, output } + return createInternalToolFilesResult(downloadedFiles, (files) => ({ + success: true, + output: { ...output, downloadedFiles: files }, + })) } diff --git a/apps/sim/lib/internal/quiver/execute-tool.test.ts b/apps/sim/lib/internal/quiver/execute-tool.test.ts index d89b4394be8..83c4e3bb4f8 100644 --- a/apps/sim/lib/internal/quiver/execute-tool.test.ts +++ b/apps/sim/lib/internal/quiver/execute-tool.test.ts @@ -4,6 +4,7 @@ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' import { DEFAULT_MAX_JSON_BODY_BYTES } from '@/lib/api/server/validation' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ executeImage: vi.fn(), @@ -16,7 +17,7 @@ vi.mock('@/lib/internal/quiver/operations', () => ({ })) import { QuiverOperationError } from '@/lib/internal/quiver/errors' -import { executeQuiverTool } from '@/lib/internal/quiver/execute-tool' +import { executeQuiverTool as executeQuiverToolOperation } from '@/lib/internal/quiver/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' function request(overrides: Partial = {}) { @@ -30,6 +31,14 @@ function request(overrides: Partial = {}) { } as InternalToolOperationCall } +async function executeQuiverTool( + request: Parameters[0] +): Promise { + const result = await executeQuiverToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeQuiverTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -47,6 +56,18 @@ describe('executeQuiverTool', () => { mocks.executeImage.mockResolvedValue(result) }) + it('forwards binary file results without serializing them', async () => { + const result = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ file }) + ) + mocks.executeText.mockResolvedValueOnce(result) + expect(await executeQuiverToolOperation(request({ toolId: 'quiver_text_to_svg_v2' }))).toBe( + result + ) + expect(mocks.executeText.mock.calls[0]?.[2]).toBe('v2') + }) + it.each([ ['quiver_text_to_svg', mocks.executeText], ['quiver_image_to_svg', mocks.executeImage], @@ -65,6 +86,28 @@ describe('executeQuiverTool', () => { ) }) + it.each([ + ['quiver_text_to_svg_v2', mocks.executeText], + ['quiver_image_to_svg_v2', mocks.executeImage], + ])('selects the stored file projection for %s', async (toolId, execute) => { + const input = + toolId === 'quiver_image_to_svg_v2' + ? { apiKey: 'secret', model: 'arrow-preview', image: 'https://example.com/image.png' } + : { apiKey: 'secret', model: 'arrow-preview', prompt: 'A compass' } + const result = createInternalToolFileResult( + { buffer: Buffer.from(''), name: 'file.svg', mimeType: 'image/svg+xml' }, + (file) => ({ success: true, output: { file, files: [file] } }) + ) + execute.mockResolvedValueOnce(result) + + expect(await executeQuiverToolOperation(request({ toolId, input }))).toBe(result) + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ apiKey: 'secret', model: 'arrow-preview' }), + expect.objectContaining({ userId: 'user-1', requestId: 'request-1' }), + 'v2' + ) + }) + it('authenticates before parsing input', async () => { const response = await executeQuiverTool( request({ input: null, context: createExecutionContext({ workflowId: 'workflow-1' }) }) diff --git a/apps/sim/lib/internal/quiver/execute-tool.ts b/apps/sim/lib/internal/quiver/execute-tool.ts index f4e4663e4d1..545023779c0 100644 --- a/apps/sim/lib/internal/quiver/execute-tool.ts +++ b/apps/sim/lib/internal/quiver/execute-tool.ts @@ -14,9 +14,11 @@ import { quiverImageToSvgInputSchema, quiverTextToSvgInputSchema, } from '@/lib/internal/quiver/schema' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import type { InternalToolOperationCall, InternalToolOperationHandler, + InternalToolOperationResult, } from '@/lib/internal/tool-operations/types' const logger = createLogger('QuiverToolExecution') @@ -44,7 +46,7 @@ async function executeOperation( request: InternalToolOperationCall, schema: z.ZodType, execute: (input: Input, context: QuiverOperationContext) => Promise -): Promise { +): Promise { request.signal?.throwIfAborted() const sizeError = validateInputSize(request.input) if (sizeError) return sizeError @@ -70,7 +72,7 @@ async function executeOperation( userId, }) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof QuiverOperationError) { @@ -89,7 +91,9 @@ async function executeOperation( } } -export const executeQuiverTool: InternalToolOperationHandler = async (request) => { +export const executeQuiverTool: InternalToolOperationHandler = async ( + request +) => { request.signal?.throwIfAborted() if (!request.context.userId) { return Response.json({ success: false, error: 'Unauthorized' }, { status: 401 }) @@ -99,6 +103,14 @@ export const executeQuiverTool: InternalToolOperationHandler = async (request) = return executeOperation(request, quiverTextToSvgInputSchema, executeQuiverTextToSvg) case 'quiver_image_to_svg': return executeOperation(request, quiverImageToSvgInputSchema, executeQuiverImageToSvg) + case 'quiver_text_to_svg_v2': + return executeOperation(request, quiverTextToSvgInputSchema, (input, context) => + executeQuiverTextToSvg(input, context, 'v2') + ) + case 'quiver_image_to_svg_v2': + return executeOperation(request, quiverImageToSvgInputSchema, (input, context) => + executeQuiverImageToSvg(input, context, 'v2') + ) default: return Response.json( { success: false, error: `Unsupported Quiver tool: ${request.toolId}` }, diff --git a/apps/sim/lib/internal/quiver/operations.test.ts b/apps/sim/lib/internal/quiver/operations.test.ts index 65fc8133fda..ca4637f6d05 100644 --- a/apps/sim/lib/internal/quiver/operations.test.ts +++ b/apps/sim/lib/internal/quiver/operations.test.ts @@ -48,6 +48,19 @@ const context = { userId: 'user-1', } +function storedSvg(name: string) { + return { + id: name, + name, + size: 14, + type: 'image/svg+xml', + mimeType: 'image/svg+xml', + url: `/api/files/${name}`, + key: `execution/${name}`, + context: 'execution' as const, + } +} + describe('Quiver operations', () => { beforeEach(() => { vi.clearAllMocks() @@ -78,7 +91,8 @@ describe('Quiver operations', () => { n: 2, temperature: 0.5, }, - { ...context, signal: controller.signal } + { ...context, signal: controller.signal }, + 'v2' ) expect(mocks.assertToolFileAccess).toHaveBeenCalledTimes(2) @@ -110,13 +124,17 @@ describe('Quiver operations', () => { }, controller.signal ) - expect(result.output).toMatchObject({ - file: { name: 'generated-1.svg', mimeType: 'image/svg+xml' }, + expect(result.files).toHaveLength(2) + const storedFiles = [storedSvg('generated-1.svg'), storedSvg('generated-2.svg')] + const presented = result.present(storedFiles) as { output: { files: unknown[] } } + expect(presented.output.files).toBe(storedFiles) + expect(presented.output).toMatchObject({ files: [{ name: 'generated-1.svg' }, { name: 'generated-2.svg' }], - svgContent: 'one', id: 'generation-1', usage: { totalTokens: 9, inputTokens: 4, outputTokens: 5 }, }) + expect(Object.keys(presented.output).sort()).toEqual(['files', 'id', 'usage']) + expect(presented.output).not.toHaveProperty('svgContent') }) it('preserves image URL inputs without reading local files', async () => { @@ -128,7 +146,8 @@ describe('Quiver operations', () => { auto_crop: false, target_size: 512, }, - context + context, + 'v2' ) expect(mocks.assertToolFileAccess).not.toHaveBeenCalled() @@ -143,11 +162,39 @@ describe('Quiver operations', () => { }, undefined ) - expect(result.output.file.name).toBe('vectorized.svg') - expect(result.output.files).toHaveLength(1) - expect(result.output.svgContent).toBe('one') + expect(result.files[0]?.name).toBe('vectorized.svg') + expect(result.files).toHaveLength(1) + const file = storedSvg('vectorized.svg') + const presented = result.present([file]) + expect(presented).toMatchObject({ + output: { files: [file] }, + }) + expect(presented).not.toHaveProperty('output.file') + expect(presented).not.toHaveProperty('output.svgContent') }) + it.each([0, 12 * 1024 * 1024])( + 'returns only stored file references for %i bytes of SVG content', + async (size) => { + const svg = `${'x'.repeat(size)}` + mocks.requestQuiverSvg.mockResolvedValue({ data: [{ svg }] }) + const result = await executeQuiverTextToSvg( + { apiKey: 'secret', model: 'arrow-preview', prompt: 'A map' }, + context, + 'v2' + ) + expect(result.files).toHaveLength(1) + expect(result.files[0]?.buffer.length).toBe(Buffer.byteLength(svg)) + const file = storedSvg('generated.svg') + const presented = result.present([file]) + expect(presented).toEqual({ + success: true, + output: { files: [file], id: null, usage: null }, + }) + expect(JSON.stringify(presented).length).toBeLessThan(1024) + } + ) + it('authorizes stored image inputs and sends their bytes', async () => { await executeQuiverImageToSvg( { apiKey: 'secret', model: 'arrow-preview', image: rawFile }, @@ -174,6 +221,46 @@ describe('Quiver operations', () => { ) }) + it('preserves v1 inline file data and every generated SVG', async () => { + const result = await executeQuiverTextToSvg( + { apiKey: 'secret', model: 'arrow-preview', prompt: 'A compass', n: 2 }, + context + ) + const files = ['one', 'two'].map((name, index) => ({ + name: `generated-${index + 1}.svg`, + mimeType: 'image/svg+xml', + data: Buffer.from(`${name}`).toString('base64'), + size: Buffer.byteLength(`${name}`), + })) + + expect(result).toEqual({ + success: true, + output: { + file: files[0], + files, + svgContent: 'one', + id: 'generation-1', + usage: { totalTokens: 9, inputTokens: 4, outputTokens: 5 }, + }, + }) + }) + + it('preserves v1 vectorization first-file projection and inline markup', async () => { + const result = await executeQuiverImageToSvg( + { apiKey: 'secret', model: 'arrow-preview', image: 'https://images.example.com/source.png' }, + context + ) + expect(result.output.files).toHaveLength(1) + expect(result.output.file).toEqual({ + name: 'vectorized.svg', + mimeType: 'image/svg+xml', + data: Buffer.from('one').toString('base64'), + size: Buffer.byteLength('one'), + }) + expect(result.output.file).toBe(result.output.files[0]) + expect(result.output.svgContent).toBe('one') + }) + it('fails closed on incomplete private model-input provenance', async () => { const headers = new Headers({ [PRIVATE_MODEL_INPUT_PROVENANCE_HEADER]: RESOLVED_SECRET_PROVENANCE_METADATA_V1, diff --git a/apps/sim/lib/internal/quiver/operations.ts b/apps/sim/lib/internal/quiver/operations.ts index 809830b7bb1..33828b10e3f 100644 --- a/apps/sim/lib/internal/quiver/operations.ts +++ b/apps/sim/lib/internal/quiver/operations.ts @@ -4,6 +4,10 @@ import { validateOpaqueModelInputProvenance } from '@/lib/execution/model-input- import { requestQuiverSvg } from '@/lib/internal/quiver/client' import { QuiverOperationError } from '@/lib/internal/quiver/errors' import type { QuiverImageToSvgInput, QuiverTextToSvgInput } from '@/lib/internal/quiver/schema' +import { + createInternalToolFilesResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import { isModelSafeWorkspaceFileKey, MODEL_UNSAFE_WORKSPACE_FILE_ERROR_MESSAGE, @@ -13,6 +17,7 @@ import type { RawFileInput } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { assertToolFileAccess } from '@/app/api/files/authorization' +import type { QuiverSvgResponse } from '@/tools/quiver/types' const logger = createLogger('QuiverOperations') @@ -23,32 +28,10 @@ export interface QuiverOperationContext { userId: string } -interface QuiverFile { - name: string - mimeType: 'image/svg+xml' - data: string - size: number -} - -interface QuiverUsage { - totalTokens: number - inputTokens: number - outputTokens: number -} - -export interface QuiverSvgOutput { - success: true - output: { - file: QuiverFile - files: QuiverFile[] - svgContent: string - id: string | null - usage: QuiverUsage | null - } -} - type ApiImage = { url: string } | { base64: string } +export type QuiverSvgOutput = QuiverSvgResponse & { success: true } + function fail(message: string, status: number, body?: Record): never { throw new QuiverOperationError(message, status, body) } @@ -155,8 +138,9 @@ async function resolveImage( function projectResult( result: unknown, fileName: (index: number, total: number) => string, + version: 'v1' | 'v2', firstOnly = false -): QuiverSvgOutput { +): QuiverSvgOutput | InternalToolFileResult { const root = record(result) const data = root.data if (!Array.isArray(data) || data.length === 0) { @@ -171,8 +155,7 @@ function projectResult( return { name: fileName(index, projectedData.length), mimeType: 'image/svg+xml' as const, - data: buffer.toString('base64'), - size: buffer.length, + buffer, } }) const usage = isRecordLike(root.usage) @@ -183,22 +166,49 @@ function projectResult( } : null - return { + if (version === 'v1') { + const inlineFiles = files.map(({ buffer, name, mimeType }) => ({ + name, + mimeType, + data: buffer.toString('base64'), + size: buffer.length, + })) + return { + success: true, + output: { + file: inlineFiles[0], + files: inlineFiles, + svgContent: record(data[0]).svg as string, + id: typeof root.id === 'string' ? root.id : null, + usage, + }, + } + } + + return createInternalToolFilesResult(files, (storedFiles) => ({ success: true, output: { - file: files[0], - files, - svgContent: record(data[0]).svg as string, + files: storedFiles, id: typeof root.id === 'string' ? root.id : null, usage, }, - } + })) } -export async function executeQuiverTextToSvg( +export function executeQuiverTextToSvg( input: QuiverTextToSvgInput, context: QuiverOperationContext -): Promise { +): Promise +export function executeQuiverTextToSvg( + input: QuiverTextToSvgInput, + context: QuiverOperationContext, + version: 'v2' +): Promise +export async function executeQuiverTextToSvg( + input: QuiverTextToSvgInput, + context: QuiverOperationContext, + version: 'v1' | 'v2' = 'v1' +): Promise { context.signal?.throwIfAborted() validateProvenance(input, context) const references: ApiImage[] = [] @@ -223,15 +233,27 @@ export async function executeQuiverTextToSvg( const result = await requestQuiverSvg('generations', input.apiKey, body, context.signal) context.signal?.throwIfAborted() - return projectResult(result, (index, total) => - total > 1 ? `generated-${index + 1}.svg` : 'generated.svg' + return projectResult( + result, + (index, total) => (total > 1 ? `generated-${index + 1}.svg` : 'generated.svg'), + version ) } -export async function executeQuiverImageToSvg( +export function executeQuiverImageToSvg( input: QuiverImageToSvgInput, context: QuiverOperationContext -): Promise { +): Promise +export function executeQuiverImageToSvg( + input: QuiverImageToSvgInput, + context: QuiverOperationContext, + version: 'v2' +): Promise +export async function executeQuiverImageToSvg( + input: QuiverImageToSvgInput, + context: QuiverOperationContext, + version: 'v1' | 'v2' = 'v1' +): Promise { context.signal?.throwIfAborted() validateProvenance(input, context) const image = await resolveImage(input.image, context) @@ -245,5 +267,5 @@ export async function executeQuiverImageToSvg( const result = await requestQuiverSvg('vectorizations', input.apiKey, body, context.signal) context.signal?.throwIfAborted() - return projectResult(result, () => 'vectorized.svg', true) + return projectResult(result, () => 'vectorized.svg', version, true) } diff --git a/apps/sim/lib/internal/sftp/execute-tool.test.ts b/apps/sim/lib/internal/sftp/execute-tool.test.ts index 901baba7b09..bf75f3c6194 100644 --- a/apps/sim/lib/internal/sftp/execute-tool.test.ts +++ b/apps/sim/lib/internal/sftp/execute-tool.test.ts @@ -2,6 +2,7 @@ * @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ executeDelete: vi.fn(), @@ -19,9 +20,9 @@ vi.mock('@/lib/internal/sftp/operations', () => ({ executeSftpUpload: mocks.executeUpload, })) -import { executeSftpTool } from '@/lib/internal/sftp/execute-tool' +import { executeSftpTool as executeSftpToolOperation } from '@/lib/internal/sftp/execute-tool' import { sftpDeleteTool } from '@/tools/sftp/delete' -import { sftpDownloadTool } from '@/tools/sftp/download' +import { sftpDownloadTool, sftpDownloadV2Tool } from '@/tools/sftp/download' import { sftpListTool } from '@/tools/sftp/list' import { sftpMkdirTool } from '@/tools/sftp/mkdir' import { sftpUploadTool } from '@/tools/sftp/upload' @@ -34,6 +35,14 @@ const baseInput = { remotePath: '/files', } +async function executeSftpTool( + request: Parameters[0] +): Promise { + const result = await executeSftpToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('SFTP tool execution', () => { beforeEach(() => { vi.clearAllMocks() @@ -42,9 +51,29 @@ describe('SFTP tool execution', () => { } }) + it('forwards binary file results without serializing them', async () => { + const result = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ file }) + ) + mocks.executeDownload.mockResolvedValueOnce(result) + expect( + await executeSftpToolOperation({ + toolId: 'sftp_download_v2', + input: baseInput, + headers: new Headers(), + context: { userId: 'user-1' }, + requestId: 'request-1', + }) + ).toBe(result) + expect(mocks.executeDownload.mock.calls[0][0]).toEqual(baseInput) + expect(mocks.executeDownload.mock.calls[0][2]).toBe('v2') + }) + it.each([ ['sftp_delete', mocks.executeDelete], ['sftp_download', mocks.executeDownload], + ['sftp_download_v2', mocks.executeDownload], ['sftp_list', mocks.executeList], ['sftp_mkdir', mocks.executeMkdir], ['sftp_upload', mocks.executeUpload], @@ -81,6 +110,7 @@ describe('SFTP tool execution', () => { for (const tool of [ sftpDeleteTool, sftpDownloadTool, + sftpDownloadV2Tool, sftpListTool, sftpMkdirTool, sftpUploadTool, diff --git a/apps/sim/lib/internal/sftp/execute-tool.ts b/apps/sim/lib/internal/sftp/execute-tool.ts index 70d5b1d947b..79ed60322dd 100644 --- a/apps/sim/lib/internal/sftp/execute-tool.ts +++ b/apps/sim/lib/internal/sftp/execute-tool.ts @@ -14,6 +14,7 @@ import { import { sftpDeleteInputSchema, sftpDownloadInputSchema, + sftpDownloadV2InputSchema, sftpListInputSchema, sftpMkdirInputSchema, sftpUploadInputSchema, @@ -21,6 +22,7 @@ import { import type { InternalToolOperationCall, InternalToolOperationHandler, + InternalToolOperationResult, } from '@/lib/internal/tool-operations/types' const logger = createLogger('SftpToolExecution') @@ -28,8 +30,11 @@ const logger = createLogger('SftpToolExecution') async function executeParsed( request: InternalToolOperationCall, schema: S, - execute: (input: z.output, context: SftpOperationContext) => Promise -): Promise { + execute: ( + input: z.output, + context: SftpOperationContext + ) => Promise +): Promise { const parsed = schema.safeParse(request.input) if (!parsed.success) { return Response.json( @@ -51,7 +56,9 @@ async function executeParsed( }) } -export const executeSftpTool: InternalToolOperationHandler = async (request) => { +export const executeSftpTool: InternalToolOperationHandler = async ( + request +) => { request.signal?.throwIfAborted() let serializedInput: string try { @@ -74,6 +81,10 @@ export const executeSftpTool: InternalToolOperationHandler = async (request) => return executeParsed(request, sftpDeleteInputSchema, executeSftpDelete) case 'sftp_download': return executeParsed(request, sftpDownloadInputSchema, executeSftpDownload) + case 'sftp_download_v2': + return executeParsed(request, sftpDownloadV2InputSchema, (input, context) => + executeSftpDownload(input, context, 'v2') + ) case 'sftp_list': return executeParsed(request, sftpListInputSchema, executeSftpList) case 'sftp_mkdir': diff --git a/apps/sim/lib/internal/sftp/operations.test.ts b/apps/sim/lib/internal/sftp/operations.test.ts index eaac26e5b46..3f07be5a173 100644 --- a/apps/sim/lib/internal/sftp/operations.test.ts +++ b/apps/sim/lib/internal/sftp/operations.test.ts @@ -66,6 +66,17 @@ const connectionInput = { } const context = { userId: 'user-1', requestId: 'request-1' } +const storedFile = { + id: 'stored-file', + name: 'file.txt', + size: 5, + type: 'text/plain', + mimeType: 'text/plain', + url: '/api/files/stored', + key: 'execution/file.txt', + context: 'execution', +} as const + describe('SFTP operations', () => { beforeEach(() => { vi.clearAllMocks() @@ -128,11 +139,74 @@ describe('SFTP operations', () => { context ) + if (!(response instanceof Response)) throw new Error('Expected a JSON response') expect(response.status).toBe(413) expect(mocks.readFile).not.toHaveBeenCalled() expect(mocks.clientEnd).toHaveBeenCalledOnce() }) + it.each([5, 12 * 1024 * 1024])( + 'returns %i bytes through a stored file without inline content in v2', + async (size) => { + const buffer = Buffer.alloc(size, 1) + const sftp = { + stat: vi.fn((_path, callback) => callback(null, { size: buffer.length })), + } as unknown as SFTPWrapper + mocks.getSftp.mockResolvedValue(sftp) + mocks.readFile.mockResolvedValue(buffer) + const result = await executeSftpDownload( + { ...connectionInput, remotePath: '/file.txt' }, + context, + 'v2' + ) + if (result instanceof Response) throw new Error('Expected a file output') + expect(result.files[0]?.buffer).toBe(buffer) + expect(result.files[0]?.name).toBe('file.txt') + const file = { ...storedFile, size: buffer.length } + const presented = result.present([file]) + expect(presented).toEqual({ file }) + expect(JSON.stringify(presented)).not.toContain('"content"') + expect(JSON.stringify(presented)).not.toContain('"encoding"') + expect(mocks.clientEnd).toHaveBeenCalledOnce() + expect(mocks.readFile).toHaveBeenCalledWith( + sftp, + '/file.txt', + 50 * 1024 * 1024, + 'SFTP download', + undefined + ) + } + ) + + it.each(['base64', 'utf-8'] as const)( + 'preserves the complete v1 %s response', + async (encoding) => { + const buffer = Buffer.from('hello') + mocks.getSftp.mockResolvedValue({ + stat: vi.fn((_path, callback) => callback(null, { size: buffer.length })), + }) + mocks.readFile.mockResolvedValue(buffer) + const result = await executeSftpDownload( + { ...connectionInput, remotePath: '/file.txt', encoding }, + context + ) + expect(await result.json()).toEqual({ + success: true, + fileName: 'file.txt', + file: { + name: 'file.txt', + mimeType: 'text/plain', + data: buffer.toString('base64'), + size: 5, + }, + content: buffer.toString(encoding), + size: 5, + encoding, + message: 'Successfully downloaded file.txt', + }) + } + ) + it('authorizes every referenced Sim file before reading or uploading it', async () => { const denied = Response.json({ success: false, error: 'File not found' }, { status: 404 }) const file = { key: 'workspace/file', name: 'private.txt', size: 4 } diff --git a/apps/sim/lib/internal/sftp/operations.ts b/apps/sim/lib/internal/sftp/operations.ts index dc82c211ba4..9949f73f755 100644 --- a/apps/sim/lib/internal/sftp/operations.ts +++ b/apps/sim/lib/internal/sftp/operations.ts @@ -23,6 +23,8 @@ import type { SftpMkdirInput, SftpUploadInput, } from '@/lib/internal/sftp/schema' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { InternalToolOperationResult } from '@/lib/internal/tool-operations/types' import { getFileExtension, getMimeTypeFromExtension, @@ -335,10 +337,20 @@ export async function executeSftpList( } } -export async function executeSftpDownload( +export function executeSftpDownload( input: SftpDownloadInput, context: SftpOperationContext -): Promise { +): Promise +export function executeSftpDownload( + input: Omit, + context: SftpOperationContext, + version: 'v2' +): Promise +export async function executeSftpDownload( + input: Omit & { encoding?: SftpDownloadInput['encoding'] }, + context: SftpOperationContext, + version: 'v1' | 'v2' = 'v1' +): Promise { if (!isPathSafe(input.remotePath)) return unsafePathResponse() try { return await withSftp(input, context, async (sftp) => { @@ -370,20 +382,25 @@ export async function executeSftpDownload( const fileName = path.basename(remotePath) const extension = getFileExtension(fileName) const mimeType = getMimeTypeFromExtension(extension) - return Response.json({ - success: true, - fileName, - file: { - name: fileName, - mimeType, - data: buffer.toString('base64'), + if (version === 'v1') { + return Response.json({ + success: true, + fileName, + file: { + name: fileName, + mimeType, + data: buffer.toString('base64'), + size: buffer.length, + }, + content: buffer.toString(input.encoding === 'base64' ? 'base64' : 'utf-8'), size: buffer.length, - }, - content: buffer.toString(input.encoding === 'base64' ? 'base64' : 'utf-8'), - size: buffer.length, - encoding: input.encoding, - message: `Successfully downloaded ${fileName}`, - }) + encoding: input.encoding, + message: `Successfully downloaded ${fileName}`, + }) + } + return createInternalToolFileResult({ buffer, name: fileName, mimeType }, (file) => ({ + file, + })) }) } catch (error) { context.signal?.throwIfAborted() diff --git a/apps/sim/lib/internal/sftp/schema.ts b/apps/sim/lib/internal/sftp/schema.ts index 6a3ab54e14d..d07447ad5be 100644 --- a/apps/sim/lib/internal/sftp/schema.ts +++ b/apps/sim/lib/internal/sftp/schema.ts @@ -52,6 +52,13 @@ export const sftpDownloadInputSchema = requireCredentials( }) ) +export const sftpDownloadV2InputSchema = requireCredentials( + z.object({ + ...connectionFields, + remotePath: z.string().min(1, 'Remote path is required'), + }) +) + export const sftpUploadInputSchema = requireCredentials( z.object({ ...connectionFields, @@ -68,4 +75,5 @@ export type SftpListInput = z.output export type SftpDeleteInput = z.output export type SftpMkdirInput = z.output export type SftpDownloadInput = z.output +export type SftpDownloadV2Input = z.output export type SftpUploadInput = z.output diff --git a/apps/sim/lib/internal/sharepoint/execute-tool.test.ts b/apps/sim/lib/internal/sharepoint/execute-tool.test.ts index d268dc14ce3..dae8f0061ce 100644 --- a/apps/sim/lib/internal/sharepoint/execute-tool.test.ts +++ b/apps/sim/lib/internal/sharepoint/execute-tool.test.ts @@ -2,6 +2,7 @@ * @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ download: vi.fn(), @@ -13,7 +14,7 @@ vi.mock('@/lib/internal/sharepoint/operations', () => ({ executeSharePointUploadFile: mocks.upload, })) -import { executeSharePointTool } from '@/lib/internal/sharepoint/execute-tool' +import { executeSharePointTool as executeSharePointToolOperation } from '@/lib/internal/sharepoint/execute-tool' import { downloadFileTool } from '@/tools/sharepoint/download_file' import { uploadFileTool } from '@/tools/sharepoint/upload_file' @@ -24,6 +25,14 @@ const context = { executionId: 'execution-1', } +async function executeSharePointTool( + request: Parameters[0] +): Promise { + const result = await executeSharePointToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeSharePointTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -53,6 +62,23 @@ describe('executeSharePointTool', () => { ) }) + it('forwards file bytes without serializing the file result', async () => { + const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ success: true, output: { file } }) + ) + mocks.download.mockResolvedValueOnce(fileResult) + expect( + await executeSharePointToolOperation({ + toolId: 'sharepoint_download_file', + input: { accessToken: 'token', driveId: 'drive', itemId: 'item' }, + headers: new Headers(), + context, + requestId: 'request-1', + }) + ).toBe(fileResult) + }) + it('requires trusted execution identity before parsing tool input', async () => { const response = await executeSharePointTool({ toolId: 'sharepoint_download_file', diff --git a/apps/sim/lib/internal/sharepoint/execute-tool.ts b/apps/sim/lib/internal/sharepoint/execute-tool.ts index 8d331672e52..c25c37b97aa 100644 --- a/apps/sim/lib/internal/sharepoint/execute-tool.ts +++ b/apps/sim/lib/internal/sharepoint/execute-tool.ts @@ -14,13 +14,17 @@ import { import type { InternalToolOperationCall, InternalToolOperationHandler, + InternalToolOperationResult, } from '@/lib/internal/tool-operations/types' async function executeParsed( request: InternalToolOperationCall, schema: S, - execute: (input: z.output, context: SharePointOperationContext) => Promise -): Promise { + execute: ( + input: z.output, + context: SharePointOperationContext + ) => Promise +): Promise { let serializedInput: string try { serializedInput = JSON.stringify(request.input) ?? '' @@ -57,7 +61,9 @@ async function executeParsed( }) } -export const executeSharePointTool: InternalToolOperationHandler = async (request) => { +export const executeSharePointTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() if (!request.context.userId) { return Response.json({ success: false, error: 'Authentication required' }, { status: 401 }) diff --git a/apps/sim/lib/internal/sharepoint/operations.test.ts b/apps/sim/lib/internal/sharepoint/operations.test.ts index 863cb6a0f6a..42b03434e1f 100644 --- a/apps/sim/lib/internal/sharepoint/operations.test.ts +++ b/apps/sim/lib/internal/sharepoint/operations.test.ts @@ -63,6 +63,17 @@ const userFile = { type: 'application/pdf', } +const storedFile = { + id: 'stored-file', + name: 'stored.bin', + size: 5, + type: 'application/octet-stream', + mimeType: 'application/octet-stream', + url: '/api/files/stored', + key: 'execution/workspace/workflow/run/stored.bin', + context: 'execution', +} as const + describe('SharePoint operations', () => { beforeEach(() => { vi.clearAllMocks() @@ -147,13 +158,14 @@ describe('SharePoint operations', () => { expect(mocks.uploadGraph).not.toHaveBeenCalled() }) - it('preserves the inline download output contract and cancellation signal', async () => { + it('keeps a large download in process until a stored file can be presented', async () => { const controller = new AbortController() mocks.getMetadata.mockResolvedValue({ name: 'source.txt', file: { mimeType: 'text/plain' }, }) - mocks.downloadGraph.mockResolvedValue(Buffer.from('hello')) + const buffer = Buffer.alloc(12 * 1024 * 1024, 1) + mocks.downloadGraph.mockResolvedValue(buffer) const response = await executeSharePointDownloadFile( { accessToken: 'token', driveId: 'drive', itemId: 'item', fileName: 'renamed.txt' }, @@ -161,16 +173,11 @@ describe('SharePoint operations', () => { ) expect(mocks.clientConstructed).toHaveBeenCalledWith('token', controller.signal) - expect(await response.json()).toEqual({ - success: true, - output: { - file: { - name: 'renamed.txt', - mimeType: 'text/plain', - data: Buffer.from('hello').toString('base64'), - size: 5, - }, - }, - }) + if (response instanceof Response) throw new Error('Expected a file output') + expect(response.files).toHaveLength(1) + expect(response.files[0]?.name).toBe('renamed.txt') + expect(response.files[0]?.mimeType).toBe('text/plain') + expect(response.files[0]?.buffer).toBe(buffer) + expect(response.present([storedFile])).toEqual({ success: true, output: { file: storedFile } }) }) }) diff --git a/apps/sim/lib/internal/sharepoint/operations.ts b/apps/sim/lib/internal/sharepoint/operations.ts index 392185a2ee9..3782a8d689b 100644 --- a/apps/sim/lib/internal/sharepoint/operations.ts +++ b/apps/sim/lib/internal/sharepoint/operations.ts @@ -10,6 +10,8 @@ import type { SharePointDownloadFileInput, SharePointUploadFileInput, } from '@/lib/internal/sharepoint/schema' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { InternalToolOperationResult } from '@/lib/internal/tool-operations/types' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' @@ -47,7 +49,7 @@ function uploadedFile(data: SharePointUploadedItem): SharePointUploadedItem { export async function executeSharePointDownloadFile( input: SharePointDownloadFileInput, context: SharePointOperationContext -): Promise { +): Promise { context.signal?.throwIfAborted() try { const client = new SharePointClient(input.accessToken, context.signal) @@ -61,17 +63,10 @@ export async function executeSharePointDownloadFile( const mimeType = metadata.file?.mimeType || 'application/octet-stream' const buffer = await client.download(input.driveId, input.itemId) context.signal?.throwIfAborted() - return Response.json({ - success: true, - output: { - file: { - name: input.fileName || metadata.name || 'download', - mimeType, - data: buffer.toString('base64'), - size: buffer.length, - }, - }, - }) + return createInternalToolFileResult( + { buffer, name: input.fileName || metadata.name || 'download', mimeType }, + (file) => ({ success: true, output: { file } }) + ) } catch (error) { context.signal?.throwIfAborted() if (error instanceof SharePointGraphError) { diff --git a/apps/sim/lib/internal/slack/execute-tool.ts b/apps/sim/lib/internal/slack/execute-tool.ts index 81905967129..f4f4cc2b131 100644 --- a/apps/sim/lib/internal/slack/execute-tool.ts +++ b/apps/sim/lib/internal/slack/execute-tool.ts @@ -28,10 +28,12 @@ import { executeSlackGetChannelHistoryOperation } from '@/lib/internal/slack/ope import { executeSlackGetThreadRepliesOperation } from '@/lib/internal/slack/operations/get-thread-replies' import { executeSlackListConversationsOperation } from '@/lib/internal/slack/operations/list-conversations' import { executeToolOperationImplementation } from '@/lib/internal/tool-operations/execute' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import { parseInternalToolInput } from '@/lib/internal/tool-operations/parse-input' import type { InternalToolOperationCall, InternalToolOperationHandler, + InternalToolOperationResult, } from '@/lib/internal/tool-operations/types' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' @@ -41,14 +43,14 @@ async function executeOperation( contract: C, request: InternalToolOperationCall, execute: (input: ContractBody) => Promise -): Promise { +): Promise { request.signal?.throwIfAborted() const parsed = parseInternalToolInput(contract, request.input) if (!parsed.success) return parsed.response try { const result = await execute(parsed.data) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof SlackOperationError) { @@ -71,7 +73,9 @@ async function executeOperation( } } -export const executeSlackTool: InternalToolOperationHandler = async (request) => { +export const executeSlackTool: InternalToolOperationHandler = async ( + request +) => { const context: SlackOperationContext = { requestId: request.requestId, signal: request.signal, diff --git a/apps/sim/lib/internal/slack/operations.test.ts b/apps/sim/lib/internal/slack/operations.test.ts index 8f269c2f2ae..b4b76baf6be 100644 --- a/apps/sim/lib/internal/slack/operations.test.ts +++ b/apps/sim/lib/internal/slack/operations.test.ts @@ -1,7 +1,12 @@ /** * @vitest-environment node */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { afterEach, assert, beforeEach, describe, expect, it, vi } from 'vitest' +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ resolveFiles: vi.fn(), @@ -195,11 +200,23 @@ describe('Slack operations', () => { }), 'uploadUrl' ) - expect(result.output).toMatchObject({ - channel: 'C1', - fileCount: 1, - ts: '10', - files: [{ name: 'hello.txt', size: 5 }], + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { name: 'hello.txt', mimeType: 'text/plain', buffer: Buffer.from('hello') }, + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', + name: 'hello.txt', + type: 'text/plain', + mimeType: 'text/plain', + size: 5, + context: 'execution', + } + expect(result.present([storedFile])).toMatchObject({ + success: true, + output: { channel: 'C1', fileCount: 1, ts: '10', files: [storedFile] }, }) }) @@ -234,12 +251,21 @@ describe('Slack operations', () => { signal: controller.signal, } ) - expect(result.output.file).toEqual({ + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { name: 'report.pdf', mimeType: 'application/pdf', buffer: Buffer.from('pdf') }, + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', name: 'report.pdf', + type: 'application/pdf', mimeType: 'application/pdf', - data: Buffer.from('pdf').toString('base64'), size: 3, - }) + context: 'execution', + } + expect(result.present([storedFile])).toEqual({ success: true, output: { file: storedFile } }) }) it.each([ diff --git a/apps/sim/lib/internal/slack/operations.ts b/apps/sim/lib/internal/slack/operations.ts index a6c139d9ccf..c2d2512330b 100644 --- a/apps/sim/lib/internal/slack/operations.ts +++ b/apps/sim/lib/internal/slack/operations.ts @@ -25,8 +25,12 @@ import { } from '@/lib/internal/slack/client' import { SlackOperationError } from '@/lib/internal/slack/errors' import { forEachSlackAttachmentFile } from '@/lib/internal/slack/file-input' +import { + createInternalToolFileResult, + createInternalToolFilesResult, + type InternalToolFile, +} from '@/lib/internal/tool-operations/file-result' import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' -import type { ToolFileData } from '@/tools/types' const logger = createLogger('SlackOperations') @@ -294,9 +298,9 @@ async function uploadSlackFiles( input: SlackSendMessageBody, channel: string, context: SlackOperationContext -): Promise<{ fileIds: string[]; files: ToolFileData[]; message?: unknown }> { +): Promise<{ fileIds: string[]; files: InternalToolFile[]; message?: unknown }> { const fileIds: string[] = [] - const files: ToolFileData[] = [] + const files: InternalToolFile[] = [] await forEachSlackAttachmentFile( input.files ?? [], @@ -344,8 +348,7 @@ async function uploadSlackFiles( files.push({ name: file.name, mimeType: file.contentType || file.type || 'application/octet-stream', - data: file.buffer.toString('base64'), - size: file.buffer.length, + buffer: file.buffer, }) } ) @@ -413,16 +416,17 @@ export async function executeSlackSendMessage( return { success: true as const, output: sentMessageOutput(data, input.text) } } - return { - success: true as const, + const { message, fileIds, files } = uploaded + return createInternalToolFilesResult(files, (storedFiles) => ({ + success: true, output: { - message: uploaded.message, - ts: record(uploaded.message).ts, + message, + ts: record(message).ts, channel, - fileCount: uploaded.fileIds.length, - files: uploaded.files, + fileCount: fileIds.length, + files: storedFiles, }, - } + })) } export async function executeSlackDownload(input: SlackDownloadBody, signal?: AbortSignal) { @@ -458,10 +462,8 @@ export async function executeSlackDownload(input: SlackDownloadBody, signal?: Ab if (!response.ok) failure(400, 'Failed to download file content') const buffer = Buffer.from(await response.arrayBuffer()) signal?.throwIfAborted() - return { - success: true as const, - output: { - file: { name, mimeType, data: buffer.toString('base64'), size: buffer.length }, - }, - } + return createInternalToolFileResult({ buffer, name, mimeType }, (file) => ({ + success: true, + output: { file }, + })) } diff --git a/apps/sim/lib/internal/ssh/execute-tool.test.ts b/apps/sim/lib/internal/ssh/execute-tool.test.ts index 7fe223caf1b..22149e60e25 100644 --- a/apps/sim/lib/internal/ssh/execute-tool.test.ts +++ b/apps/sim/lib/internal/ssh/execute-tool.test.ts @@ -3,6 +3,7 @@ */ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const operationMocks = vi.hoisted(() => ({ executeSshCheckCommandExists: vi.fn(), @@ -24,7 +25,7 @@ vi.mock('@/lib/internal/ssh/operations', () => operationMocks) import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { SshOperationError } from '@/lib/internal/ssh/errors' -import { executeSshTool } from '@/lib/internal/ssh/execute-tool' +import { executeSshTool as executeSshToolOperation } from '@/lib/internal/ssh/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' const CONNECTION = { @@ -40,6 +41,7 @@ const TOOL_IDS = [ 'ssh_create_directory', 'ssh_delete_file', 'ssh_download_file', + 'ssh_download_file_v2', 'ssh_execute_command', 'ssh_execute_script', 'ssh_get_system_info', @@ -67,6 +69,14 @@ function createRequest( } } +async function executeSshTool( + request: Parameters[0] +): Promise { + const result = await executeSshToolOperation(request) + if (!(result instanceof Response)) throw new Error('Expected a JSON response') + return result +} + describe('executeSshTool', () => { beforeEach(() => { vi.clearAllMocks() @@ -75,6 +85,23 @@ describe('executeSshTool', () => { } }) + it('forwards binary file results without serializing them', async () => { + const result = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.txt', mimeType: 'text/plain' }, + (file) => ({ file }) + ) + operationMocks.executeSshDownloadFile.mockResolvedValueOnce(result) + expect( + await executeSshToolOperation( + createRequest({ + toolId: 'ssh_download_file_v2', + input: { ...CONNECTION, remotePath: '/file.txt' }, + }) + ) + ).toBe(result) + expect(operationMocks.executeSshDownloadFile.mock.calls[0][2]).toBe('v2') + }) + it('validates typed operation input and dispatches without reading a serialized body', async () => { const controller = new AbortController() const input = { ...CONNECTION, command: 'pwd' } diff --git a/apps/sim/lib/internal/ssh/execute-tool.ts b/apps/sim/lib/internal/ssh/execute-tool.ts index 58010742226..b1261db2d27 100644 --- a/apps/sim/lib/internal/ssh/execute-tool.ts +++ b/apps/sim/lib/internal/ssh/execute-tool.ts @@ -34,9 +34,11 @@ import { executeSshWriteFileContent, type SshOperationContext, } from '@/lib/internal/ssh/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import type { InternalToolOperationCall, InternalToolOperationHandler, + InternalToolOperationResult, } from '@/lib/internal/tool-operations/types' const logger = createLogger('SshToolExecution') @@ -46,7 +48,7 @@ async function executeOperation( request: InternalToolOperationCall, operation: (input: ContractBody, context: SshOperationContext) => Promise, failureMessage: string -): Promise { +): Promise { request.signal?.throwIfAborted() if (!contract.body) throw new Error(`SSH contract ${contract.path} has no operation input`) const parsed = contract.body.safeParse(request.input) @@ -60,7 +62,7 @@ async function executeOperation( try { const result = await operation(parsed.data as ContractBody, { signal: request.signal }) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof SshOperationError) { @@ -79,7 +81,9 @@ async function executeOperation( } } -export const executeSshTool: InternalToolOperationHandler = async (request) => { +export const executeSshTool: InternalToolOperationHandler = async ( + request +) => { switch (request.toolId) { case 'ssh_check_command_exists': return executeOperation( @@ -116,6 +120,13 @@ export const executeSshTool: InternalToolOperationHandler = async (request) => { executeSshDownloadFile, 'SSH file download failed' ) + case 'ssh_download_file_v2': + return executeOperation( + sshDownloadFileContract, + request, + (input, context) => executeSshDownloadFile(input, context, 'v2'), + 'SSH file download failed' + ) case 'ssh_execute_command': return executeOperation( sshExecuteCommandContract, diff --git a/apps/sim/lib/internal/ssh/operations.test.ts b/apps/sim/lib/internal/ssh/operations.test.ts index 5927e232ed4..99b394924b1 100644 --- a/apps/sim/lib/internal/ssh/operations.test.ts +++ b/apps/sim/lib/internal/ssh/operations.test.ts @@ -1,10 +1,12 @@ +import { Readable } from 'node:stream' /** * @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ - client: { destroy: vi.fn(), end: vi.fn() }, + client: { destroy: vi.fn(), end: vi.fn(), sftp: vi.fn() }, createSSHConnection: vi.fn(), executeSSHCommand: vi.fn(), })) @@ -19,7 +21,7 @@ vi.mock('@/lib/internal/ssh/client', () => ({ sanitizePath: (value: string) => value.trim(), })) -import { executeSshExecuteCommand } from '@/lib/internal/ssh/operations' +import { executeSshDownloadFile, executeSshExecuteCommand } from '@/lib/internal/ssh/operations' const INPUT = { host: 'ssh.example.com', @@ -30,6 +32,17 @@ const INPUT = { workingDirectory: "/srv/app's", } +const storedFile = { + id: 'stored-file', + name: 'file.txt', + size: 5, + type: 'text/plain', + mimeType: 'text/plain', + url: '/api/files/stored', + key: 'execution/file.txt', + context: 'execution', +} as const + describe('SSH operations', () => { beforeEach(() => { vi.clearAllMocks() @@ -58,6 +71,65 @@ describe('SSH operations', () => { expect(mocks.client.end).toHaveBeenCalledOnce() }) + it.each([5, 12 * 1024 * 1024])( + 'downloads %i bytes through a stored file without inline content in v2', + async (size) => { + const buffer = Buffer.alloc(size, 65) + const sftp = { + stat: vi.fn((_path, callback) => callback(null, { size })), + createReadStream: vi.fn(() => Readable.from([buffer])), + } + mocks.client.sftp.mockImplementation((callback) => callback(null, sftp)) + const result = await executeSshDownloadFile( + { + host: INPUT.host, + port: INPUT.port, + username: INPUT.username, + password: INPUT.password, + remotePath: '/file.txt', + }, + {}, + 'v2' + ) + if (!isInternalToolFileResult(result)) throw new Error('Expected a file output') + expect(result.files[0]?.buffer.length).toBe(size) + expect(result.files[0]?.name).toBe('file.txt') + const file = { ...storedFile, size } + const presented = result.present([file]) + expect(presented).toEqual({ + file, + remotePath: '/file.txt', + }) + expect(JSON.stringify(presented)).not.toContain('"content"') + expect(sftp.createReadStream).toHaveBeenCalledOnce() + expect(mocks.client.end).toHaveBeenCalledOnce() + } + ) + + it('preserves the complete legacy v1 download response', async () => { + const buffer = Buffer.from('hello') + const sftp = { + stat: vi.fn((_path, callback) => callback(null, { size: buffer.length })), + createReadStream: vi.fn(() => Readable.from([buffer])), + } + mocks.client.sftp.mockImplementation((callback) => callback(null, sftp)) + const result = await executeSshDownloadFile({ ...INPUT, remotePath: '/file.txt' }, {}) + expect(result).toEqual({ + downloaded: true, + file: { + name: 'file.txt', + mimeType: 'text/plain', + data: buffer.toString('base64'), + size: 5, + }, + content: buffer.toString('base64'), + fileName: 'file.txt', + remotePath: '/file.txt', + size: 5, + message: 'File downloaded successfully from /file.txt', + }) + }) + it('destroys and closes the client when cancellation wins during provider work', async () => { const controller = new AbortController() mocks.executeSSHCommand.mockImplementationOnce(async () => { diff --git a/apps/sim/lib/internal/ssh/operations.ts b/apps/sim/lib/internal/ssh/operations.ts index 0072c49f4aa..7970d848c99 100644 --- a/apps/sim/lib/internal/ssh/operations.ts +++ b/apps/sim/lib/internal/ssh/operations.ts @@ -33,6 +33,7 @@ import { sanitizePath, } from '@/lib/internal/ssh/client' import { SshOperationError } from '@/lib/internal/ssh/errors' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' export interface SshOperationContext { @@ -281,7 +282,8 @@ export async function executeSshDeleteFile( export async function executeSshDownloadFile( input: DownloadFileInput, - context: SshOperationContext + context: SshOperationContext, + version: 'v1' | 'v2' = 'v1' ): Promise { return withClient(input, context, async (client) => { const sftp = await getSftp(client, context.signal) @@ -307,21 +309,34 @@ export async function executeSshDownloadFile( context.signal ) const fileName = path.basename(remotePath) - const base64Content = content.toString('base64') - return { - downloaded: true, - file: { + if (version === 'v1') { + const base64Content = content.toString('base64') + return { + downloaded: true, + file: { + name: fileName, + mimeType: getMimeTypeFromExtension(getFileExtension(fileName)), + data: base64Content, + size: content.length, + }, + content: base64Content, + fileName, + remotePath, + size: content.length, + message: `File downloaded successfully from ${remotePath}`, + } + } + return createInternalToolFileResult( + { + buffer: content, name: fileName, mimeType: getMimeTypeFromExtension(getFileExtension(fileName)), - data: base64Content, - size: content.length, }, - content: base64Content, - fileName, - remotePath, - size: content.length, - message: `File downloaded successfully from ${remotePath}`, - } + (file) => ({ + file, + remotePath, + }) + ) }) } diff --git a/apps/sim/lib/internal/telegram/execute-tool.test.ts b/apps/sim/lib/internal/telegram/execute-tool.test.ts index 8dd8f96435b..a71d51157f0 100644 --- a/apps/sim/lib/internal/telegram/execute-tool.test.ts +++ b/apps/sim/lib/internal/telegram/execute-tool.test.ts @@ -1,8 +1,10 @@ /** * @vitest-environment node */ + import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ sendTelegramDocument: vi.fn() })) @@ -13,10 +15,15 @@ vi.mock('@/lib/internal/telegram/operations', () => ({ import { executeTelegramTool } from '@/lib/internal/telegram/execute-tool' import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' +const fileResult = createInternalToolFileResult( + { buffer: Buffer.from('file'), name: 'file.pdf', mimeType: 'application/pdf' }, + (file) => ({ success: true, output: { file } }) +) + describe('executeTelegramTool', () => { beforeEach(() => { vi.clearAllMocks() - mocks.sendTelegramDocument.mockResolvedValue({ success: true, output: {} }) + mocks.sendTelegramDocument.mockResolvedValue(fileResult) }) it('uses trusted user context for protected files', async () => { @@ -35,7 +42,7 @@ describe('executeTelegramTool', () => { signal: controller.signal, } - expect((await executeTelegramTool(request)).status).toBe(200) + expect(await executeTelegramTool(request)).toBe(fileResult) expect(mocks.sendTelegramDocument).toHaveBeenCalledWith(input, { userId: 'user-1', requestId: 'request-1', diff --git a/apps/sim/lib/internal/telegram/execute-tool.ts b/apps/sim/lib/internal/telegram/execute-tool.ts index ae7f4d27200..eb0f24ba471 100644 --- a/apps/sim/lib/internal/telegram/execute-tool.ts +++ b/apps/sim/lib/internal/telegram/execute-tool.ts @@ -2,7 +2,10 @@ import { getErrorMessage } from '@sim/utils/errors' import { z } from 'zod' import { TelegramOperationError } from '@/lib/internal/telegram/errors' import { sendTelegramDocument } from '@/lib/internal/telegram/operations' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' @@ -13,7 +16,9 @@ const inputSchema = z.object({ caption: z.string().optional().nullable(), }) -export const executeTelegramTool: InternalToolOperationHandler = async (request) => { +export const executeTelegramTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() if (request.toolId !== 'telegram_send_document') { return Response.json( @@ -30,13 +35,11 @@ export const executeTelegramTool: InternalToolOperationHandler = async (request) return Response.json({ success: false, error: 'Invalid request data' }, { status: 400 }) } try { - return Response.json( - await sendTelegramDocument(parsed.data, { - userId, - requestId: request.requestId, - signal: request.signal, - }) - ) + return await sendTelegramDocument(parsed.data, { + userId, + requestId: request.requestId, + signal: request.signal, + }) } catch (error) { request.signal?.throwIfAborted() const notReady = docNotReadyResponse(error) diff --git a/apps/sim/lib/internal/telegram/operations.test.ts b/apps/sim/lib/internal/telegram/operations.test.ts index 862acbaae0a..1ca7855cac9 100644 --- a/apps/sim/lib/internal/telegram/operations.test.ts +++ b/apps/sim/lib/internal/telegram/operations.test.ts @@ -1,7 +1,12 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { assert, beforeEach, describe, expect, it, vi } from 'vitest' +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ assertToolFileAccess: vi.fn(), @@ -53,9 +58,28 @@ describe('sendTelegramDocument', () => { expect(mocks.fetch.mock.calls[0][1]).toEqual( expect.objectContaining({ signal: controller.signal }) ) - expect(result.output.files?.[0]).toEqual( - expect.objectContaining({ name: 'file.pdf', data: 'AQID', size: 3 }) - ) + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { name: 'file.pdf', mimeType: 'application/pdf', buffer: Buffer.from([1, 2, 3]) }, + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', + name: 'file.pdf', + type: 'application/pdf', + mimeType: 'application/pdf', + size: 3, + context: 'execution', + } + expect(result.present([storedFile])).toEqual({ + success: true, + output: { + message: 'Document sent successfully', + data: { message_id: 1 }, + files: [storedFile], + }, + }) }) it('fails closed before materialization when file access is denied', async () => { diff --git a/apps/sim/lib/internal/telegram/operations.ts b/apps/sim/lib/internal/telegram/operations.ts index aa18bdaf50d..b321ac86ee3 100644 --- a/apps/sim/lib/internal/telegram/operations.ts +++ b/apps/sim/lib/internal/telegram/operations.ts @@ -2,6 +2,10 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { isPayloadSizeLimitError, readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { TelegramOperationError } from '@/lib/internal/telegram/errors' +import { + createInternalToolFileResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import type { RawFileInput } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' @@ -35,7 +39,7 @@ interface TelegramApiResponse { export async function sendTelegramDocument( input: TelegramSendDocumentInput, context: TelegramOperationContext -): Promise { +): Promise { context.signal?.throwIfAborted() if (!input.files?.length) { throw new TelegramOperationError( @@ -118,19 +122,12 @@ export async function sendTelegramDocument( ) } - return { + return createInternalToolFileResult({ buffer, name: userFile.name, mimeType }, (file) => ({ success: true, output: { message: 'Document sent successfully', data: data.result, - files: [ - { - name: userFile.name, - mimeType, - data: buffer.toString('base64'), - size: buffer.length, - }, - ], + files: [file], }, - } + })) } diff --git a/apps/sim/lib/internal/tool-operations/file-result.server.test.ts b/apps/sim/lib/internal/tool-operations/file-result.server.test.ts new file mode 100644 index 00000000000..5a5dcc9c636 --- /dev/null +++ b/apps/sim/lib/internal/tool-operations/file-result.server.test.ts @@ -0,0 +1,527 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + PayloadSizeLimitError, + readResponseToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' +import { + createInternalToolFileResult, + createInternalToolFilesResult, + type InternalToolFile, +} from '@/lib/internal/tool-operations/file-result' +import { MAX_TOOL_RESPONSE_BODY_BYTES } from '@/lib/internal/tool-operations/response-limits' +import type { InternalToolOperationContext } from '@/lib/internal/tool-operations/types' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' +import type { UserFile } from '@/executor/types' + +const mocks = vi.hoisted(() => ({ + uploadExecution: vi.fn(), + uploadCopilot: vi.fn(), + deleteFile: vi.fn(), + deleteMetadata: vi.fn(), +})) + +vi.mock('@/lib/uploads/contexts/execution', () => ({ + uploadExecutionFile: mocks.uploadExecution, +})) + +vi.mock('@/lib/uploads/contexts/copilot', () => ({ + uploadCopilotFile: mocks.uploadCopilot, +})) + +vi.mock('@/lib/uploads/core/storage-service', () => ({ deleteFile: mocks.deleteFile })) +vi.mock('@/lib/uploads/server/metadata', () => ({ deleteFileMetadata: mocks.deleteMetadata })) + +import { + presentInternalToolOperationResult, + storeInternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result.server' + +const runContext: InternalToolOperationContext = { + workspaceId: 'workspace-1', + workflowId: 'workflow-1', + executionId: 'execution-1', + userId: 'user-1', +} + +const copilotContext: InternalToolOperationContext = { + workspaceId: 'workspace-1', + workflowId: '', + userId: 'user-1', + copilotToolExecution: true, +} + +function file(buffer = Buffer.from('workbook')): InternalToolFile { + return { + buffer, + name: 'workbook.xlsx', + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + } +} + +function storedFile( + buffer: Buffer, + name: string, + type: string, + context: 'execution' | 'copilot', + index = 1 +): UserFile { + return { + id: `file-${index}`, + key: `${context}/file-${index}/${name}`, + url: `https://storage.example/file-${index}`, + name, + size: buffer.length, + type, + context, + } +} + +describe('presentInternalToolOperationResult', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.uploadExecution.mockReset() + mocks.uploadCopilot.mockReset() + mocks.deleteFile.mockReset() + mocks.deleteMetadata.mockReset() + mocks.uploadExecution.mockImplementation( + async (_scope: unknown, buffer: Buffer, name: string, type: string) => + storedFile(buffer, name, type, 'execution', mocks.uploadExecution.mock.calls.length) + ) + mocks.uploadCopilot.mockImplementation( + async (input: { buffer: Buffer; fileName: string; contentType: string }) => + storedFile(input.buffer, input.fileName, input.contentType, 'copilot') + ) + mocks.deleteFile.mockResolvedValue(undefined) + mocks.deleteMetadata.mockResolvedValue(true) + }) + + it('passes ordinary responses through without reading or changing them', async () => { + const original = Response.json({ error: 'Provider failed' }, { status: 403 }) + const controller = new AbortController() + controller.abort() + + const result = await presentInternalToolOperationResult( + original, + { workflowId: '' }, + controller.signal + ) + + expect(result).toBe(original) + expect(original.bodyUsed).toBe(false) + expect(mocks.uploadExecution).not.toHaveBeenCalled() + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + }) + + it('persists a 12 MiB workbook before serializing its descriptor and adjacent metadata', async () => { + const input = file(Buffer.alloc(12 * 1024 * 1024, 1)) + const result = createInternalToolFileResult( + input, + (stored) => ({ success: true, output: { file: stored, metadata: { sourceId: 'item-1' } } }), + { status: 201, headers: { 'x-provider-version': 'v1' } } + ) + + const response = await presentInternalToolOperationResult(result, runContext) + const body = await response.text() + + expect(body.length).toBeLessThan(1024) + expect(response.status).toBe(201) + expect(response.headers.get('x-provider-version')).toBe('v1') + expect(response.headers.get('content-type')).toBe('application/json') + expect(JSON.parse(body)).toMatchObject({ + success: true, + output: { + file: { + name: input.name, + size: input.buffer.length, + type: input.mimeType, + }, + metadata: { sourceId: 'item-1' }, + }, + }) + const [scope, buffer, name, mimeType, userId] = mocks.uploadExecution.mock.calls[0]! + expect(buffer).toBe(input.buffer) + expect({ scope, name, mimeType, userId }).toEqual({ + scope: { workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1' }, + name: input.name, + mimeType: input.mimeType, + userId: 'user-1', + }) + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + }) + + it('stores non-run files under the authenticated Copilot user', async () => { + const input = file() + const response = await presentInternalToolOperationResult( + createInternalToolFileResult(input, (stored) => ({ file: stored, fileUrl: stored.url })), + copilotContext + ) + + expect(await response.json()).toMatchObject({ + file: { context: 'copilot' }, + fileUrl: 'https://storage.example/file-1', + }) + expect(mocks.uploadCopilot).toHaveBeenCalledWith({ + buffer: input.buffer, + fileName: input.name, + contentType: input.mimeType, + userId: 'user-1', + }) + expect(mocks.uploadExecution).not.toHaveBeenCalled() + }) + + it('replaces binary representation headers before the JSON transport size check', async () => { + const input = file(Buffer.alloc(12 * 1024 * 1024)) + const headers = new Headers({ + 'content-length': String(input.buffer.length), + 'content-encoding': 'gzip', + 'content-type': input.mimeType, + 'x-provider-version': 'v1', + }) + const response = await presentInternalToolOperationResult( + createInternalToolFileResult(input, (stored) => ({ file: stored }), { + status: 201, + statusText: 'Created', + headers, + }), + runContext + ) + + const body = await readResponseToBufferWithLimit(response, { + maxBytes: MAX_TOOL_RESPONSE_BODY_BYTES, + label: 'Tool response body', + }) + + expect(body.length).toBeLessThan(1024) + expect(JSON.parse(body.toString('utf8'))).toMatchObject({ + file: { size: input.buffer.length, context: 'execution' }, + }) + expect(response.status).toBe(201) + expect(response.statusText).toBe('Created') + expect(response.headers.get('content-type')).toBe('application/json') + expect(response.headers.get('content-length')).toBeNull() + expect(response.headers.get('content-encoding')).toBeNull() + expect(response.headers.get('x-provider-version')).toBe('v1') + expect(headers.get('content-length')).toBe(String(input.buffer.length)) + expect(mocks.uploadExecution).toHaveBeenCalledTimes(1) + expect(mocks.deleteFile).not.toHaveBeenCalled() + }) + + it('allows actorless execution artifacts without requiring a human subject', async () => { + const result = createInternalToolFileResult(file(), (stored) => ({ file: stored })) + await presentInternalToolOperationResult(result, { + ...runContext, + userId: undefined, + executorDelegationOrigin: { + workflowId: 'workflow-1', + executionId: 'execution-1', + principal: { + kind: 'system', + serviceId: 'schedule', + workspaceId: 'workspace-1', + workflowId: 'workflow-1', + }, + }, + }) + + expect(mocks.uploadExecution.mock.calls[0]?.[4]).toBeUndefined() + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + }) + + it("does not make an actorless principal's compatibility owner a Copilot user", async () => { + await expect( + presentInternalToolOperationResult( + createInternalToolFileResult(file(), (stored) => ({ file: stored })), + { + ...copilotContext, + userId: 'billing-owner', + executorDelegationOrigin: { + workflowId: 'workflow-1', + principal: { + kind: 'system', + serviceId: 'schedule', + workspaceId: 'workspace-1', + workflowId: 'workflow-1', + }, + }, + } + ) + ).rejects.toThrow('human subject') + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + }) + + it('uses a real principal subject and refuses a conflicting claimed owner', async () => { + const result = createInternalToolFileResult(file(), (stored) => ({ file: stored })) + const context: InternalToolOperationContext = { + ...copilotContext, + userId: undefined, + executorDelegationOrigin: { + workflowId: 'workflow-1', + principal: { kind: 'session', userId: 'actual-user', sessionId: 'session-1' }, + }, + } + await presentInternalToolOperationResult(result, context) + expect(mocks.uploadCopilot).toHaveBeenCalledWith( + expect.objectContaining({ userId: 'actual-user' }) + ) + + await expect( + presentInternalToolOperationResult(result, { ...context, userId: 'other-user' }) + ).rejects.toThrow('does not match') + expect(mocks.uploadCopilot).toHaveBeenCalledTimes(1) + }) + + it.each([ + { ...copilotContext, userId: undefined }, + { ...runContext, workspaceId: undefined }, + { ...runContext, workflowId: '' }, + ])('rejects missing storage authority before uploading: %j', async (context) => { + await expect( + presentInternalToolOperationResult( + createInternalToolFileResult(file(), (stored) => ({ file: stored })), + context + ) + ).rejects.toThrow() + expect(mocks.uploadExecution).not.toHaveBeenCalled() + expect(mocks.uploadCopilot).not.toHaveBeenCalled() + }) + + it('accepts exactly 100 MiB and rejects larger files before uploading', async () => { + const atLimit = file(Buffer.alloc(MAX_BUFFERED_TRANSFER_BYTES)) + await presentInternalToolOperationResult( + createInternalToolFileResult(atLimit, (stored) => ({ file: stored })), + runContext + ) + expect(mocks.uploadExecution).toHaveBeenCalledTimes(1) + + const oversized = file(Buffer.alloc(MAX_BUFFERED_TRANSFER_BYTES + 1)) + await expect( + presentInternalToolOperationResult( + createInternalToolFileResult(oversized, (stored) => ({ file: stored })), + runContext + ) + ).rejects.toBeInstanceOf(PayloadSizeLimitError) + expect(mocks.uploadExecution).toHaveBeenCalledTimes(1) + }) + + it('checks the aggregate buffer budget before storing the first file', async () => { + const files = [file(Buffer.alloc(60 * 1024 * 1024)), file(Buffer.alloc(41 * 1024 * 1024))] + await expect( + presentInternalToolOperationResult( + createInternalToolFilesResult(files, (stored) => ({ files: stored })), + runContext + ) + ).rejects.toMatchObject({ + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + observedBytes: 101 * 1024 * 1024, + }) + expect(mocks.uploadExecution).not.toHaveBeenCalled() + }) + + it('validates every file before any upload', async () => { + await expect( + presentInternalToolOperationResult( + createInternalToolFilesResult([file(), { ...file(), name: ' ' }], (stored) => ({ + files: stored, + })), + runContext + ) + ).rejects.toThrow('filename') + expect(mocks.uploadExecution).not.toHaveBeenCalled() + }) + + it('persists distinct files sequentially and reuses repeated file references', async () => { + const first = file() + const second = { ...file(), name: 'second.xlsx' } + let firstFinished = false + mocks.uploadExecution + .mockImplementationOnce(async () => { + await Promise.resolve() + firstFinished = true + return storedFile(first.buffer, first.name, first.mimeType, 'execution') + }) + .mockImplementationOnce(async () => { + expect(firstFinished).toBe(true) + return storedFile(second.buffer, second.name, second.mimeType, 'execution', 2) + }) + const result = createInternalToolFilesResult([first, second, first], (stored) => { + expect(stored[0]).toBe(stored[2]) + return { files: stored, echoedFile: stored[0] } + }) + + const response = await presentInternalToolOperationResult(result, runContext) + expect((await response.json()).files).toHaveLength(3) + expect(mocks.uploadExecution).toHaveBeenCalledTimes(2) + }) + + it('preserves image sniffing before returning an already stored descriptor', async () => { + const input = { buffer: Buffer.from(''), name: 'image.png', mimeType: 'image/png' } + const response = await presentInternalToolOperationResult( + createInternalToolFileResult(input, (stored) => ({ file: stored })), + runContext + ) + + expect(mocks.uploadExecution).toHaveBeenCalledWith( + expect.anything(), + input.buffer, + 'image.bin', + 'application/octet-stream', + 'user-1' + ) + expect(await response.json()).toMatchObject({ + file: { + name: 'image.bin', + type: 'application/octet-stream', + }, + }) + }) + + it('does not upload after cancellation', async () => { + const controller = new AbortController() + const error = new Error('cancelled') + controller.abort(error) + await expect( + presentInternalToolOperationResult( + createInternalToolFileResult(file(), (stored) => ({ file: stored })), + runContext, + controller.signal + ) + ).rejects.toBe(error) + expect(mocks.uploadExecution).not.toHaveBeenCalled() + }) + + it('rolls back an upload that completed after cancellation, without the aborted signal', async () => { + const controller = new AbortController() + const error = new Error('cancelled during upload') + mocks.uploadExecution.mockImplementationOnce(async () => { + controller.abort(error) + return storedFile(Buffer.from('file'), 'file.txt', 'text/plain', 'execution') + }) + + await expect( + presentInternalToolOperationResult( + createInternalToolFilesResult([file(), file()], (stored) => ({ files: stored })), + runContext, + controller.signal + ) + ).rejects.toBe(error) + expect(mocks.uploadExecution).toHaveBeenCalledTimes(1) + expect(mocks.deleteFile).toHaveBeenCalledWith({ + key: 'execution/file-1/file.txt', + context: 'execution', + }) + expect(mocks.deleteMetadata).toHaveBeenCalledWith('execution/file-1/file.txt') + }) + + it('rolls back preceding uploads when a later upload fails', async () => { + const error = new Error('Storage unavailable') + mocks.uploadExecution + .mockResolvedValueOnce( + storedFile(Buffer.from('file'), 'first.txt', 'text/plain', 'execution') + ) + .mockRejectedValueOnce(error) + await expect( + presentInternalToolOperationResult( + createInternalToolFilesResult([file(), file()], (stored) => ({ files: stored })), + runContext + ) + ).rejects.toBe(error) + expect(mocks.deleteFile).toHaveBeenCalledTimes(1) + expect(mocks.deleteMetadata).toHaveBeenCalledWith('execution/file-1/first.txt') + }) + + it.each([ + () => { + throw new Error('Presentation failed') + }, + () => ({ unsupported: 1n }), + () => undefined, + ])('rolls back Copilot files if presentation or serialization fails', async (present) => { + await expect( + presentInternalToolOperationResult( + createInternalToolFileResult(file(), present), + copilotContext + ) + ).rejects.toThrow() + expect(mocks.deleteFile).toHaveBeenCalledWith({ + key: 'copilot/file-1/workbook.xlsx', + context: 'copilot', + }) + expect(mocks.deleteMetadata).toHaveBeenCalledWith('copilot/file-1/workbook.xlsx') + }) + + it('rolls back when adjacent JSON exceeds the unchanged transport limit', async () => { + await expect( + presentInternalToolOperationResult( + createInternalToolFileResult(file(), (stored) => ({ + file: stored, + text: 'x'.repeat(MAX_TOOL_RESPONSE_BODY_BYTES), + })), + runContext + ) + ).rejects.toMatchObject({ maxBytes: MAX_TOOL_RESPONSE_BODY_BYTES }) + expect(mocks.deleteFile).toHaveBeenCalledTimes(1) + expect(mocks.deleteMetadata).toHaveBeenCalledTimes(1) + }) + + it('finalizes large binary outputs as stored file descriptors', async () => { + const buffer = Buffer.alloc(12 * 1024 * 1024) + const finalize = vi.fn((body: unknown) => body) + const output = await storeInternalToolFileResult( + createInternalToolFileResult(file(buffer), (stored) => ({ + success: true, + output: { file: stored }, + })), + copilotContext, + finalize + ) + + expect(output).toBe(finalize.mock.calls[0]?.[0]) + expect(output).toMatchObject({ + success: true, + output: { file: { context: 'copilot', size: buffer.length } }, + }) + expect(output).not.toHaveProperty('output.file.data') + expect(JSON.stringify(output).length).toBeLessThan(1024) + expect(mocks.uploadCopilot).toHaveBeenCalledTimes(1) + expect(mocks.uploadCopilot.mock.calls[0]?.[0].buffer).toBe(buffer) + expect(mocks.deleteFile).not.toHaveBeenCalled() + }) + + it('rolls back storage if the external result finalizer rejects its output', async () => { + const error = new Error('Invalid tool response') + await expect( + storeInternalToolFileResult( + createInternalToolFileResult(file(), (stored) => ({ file: stored })), + copilotContext, + () => { + throw error + } + ) + ).rejects.toBe(error) + + expect(mocks.deleteFile).toHaveBeenCalledWith({ + key: 'copilot/file-1/workbook.xlsx', + context: 'copilot', + }) + expect(mocks.deleteMetadata).toHaveBeenCalledTimes(1) + }) + + it('attempts remaining cleanup after a deletion failure and preserves the original error', async () => { + const error = new Error('Presentation failed') + mocks.deleteFile.mockRejectedValueOnce(new Error('Cleanup failed')) + await expect( + presentInternalToolOperationResult( + createInternalToolFilesResult([file(), { ...file(), name: 'second.xlsx' }], () => { + throw error + }), + runContext + ) + ).rejects.toBe(error) + expect(mocks.deleteFile).toHaveBeenCalledTimes(2) + expect(mocks.deleteMetadata).toHaveBeenCalledTimes(1) + expect(mocks.deleteMetadata).toHaveBeenCalledWith('execution/file-2/second.xlsx') + }) +}) diff --git a/apps/sim/lib/internal/tool-operations/file-result.server.ts b/apps/sim/lib/internal/tool-operations/file-result.server.ts new file mode 100644 index 00000000000..ab1b6e516d9 --- /dev/null +++ b/apps/sim/lib/internal/tool-operations/file-result.server.ts @@ -0,0 +1,185 @@ +import { PrincipalSubjectUserRequiredError, resolvePrincipalSubject } from '@sim/auth/principal' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { omit } from '@sim/utils/object' +import { assertKnownSizeWithinLimit } from '@/lib/core/utils/stream-limits' +import type { + InternalToolFile, + InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' +import { MAX_TOOL_RESPONSE_BODY_BYTES } from '@/lib/internal/tool-operations/response-limits' +import type { + InternalToolOperationContext, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' +import { uploadCopilotFile } from '@/lib/uploads/contexts/copilot' +import { uploadExecutionFile } from '@/lib/uploads/contexts/execution' +import type { ExecutionContext } from '@/lib/uploads/contexts/execution/utils' +import { deleteFile } from '@/lib/uploads/core/storage-service' +import { deleteFileMetadata } from '@/lib/uploads/server/metadata' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' +import { resolveStoredFileMetadata } from '@/lib/uploads/utils/stored-file-metadata' +import type { UserFile } from '@/executor/types' + +const logger = createLogger('InternalToolFileResult') + +type FileStorageScope = + | { kind: 'execution'; context: ExecutionContext; userId?: string } + | { kind: 'copilot'; userId: string } + +interface CreatedFile { + key: string + context: FileStorageScope['kind'] +} + +function resolveCopilotUserId(context: InternalToolOperationContext): string { + const origin = context.executorDelegationOrigin + const principal = origin?.principal + const subject = principal ? resolvePrincipalSubject(principal) : null + if (principal && subject?.kind !== 'sim_user') { + throw new PrincipalSubjectUserRequiredError(principal.kind) + } + const userId = + subject?.kind === 'sim_user' ? subject.userId : (origin?.subjectUserId ?? context.userId) + if (!userId?.trim()) throw new Error('Authentication required') + if ( + (origin?.subjectUserId !== undefined && origin.subjectUserId !== userId) || + (context.userId !== undefined && context.userId !== userId) + ) { + throw new Error('Tool file owner does not match the authenticated subject') + } + return userId +} + +function resolveFileStorageScope(context: InternalToolOperationContext): FileStorageScope { + if (context.executionId) { + if (!context.workspaceId?.trim() || !context.workflowId.trim() || !context.executionId.trim()) { + throw new Error('Execution file output requires a complete trusted execution scope') + } + return { + kind: 'execution', + context: { + workspaceId: context.workspaceId, + workflowId: context.workflowId, + executionId: context.executionId, + }, + userId: context.userId, + } + } + return { kind: 'copilot', userId: resolveCopilotUserId(context) } +} + +function validateFiles(result: InternalToolFileResult): readonly InternalToolFile[] { + const files = [...new Set(result.files)] + let totalBytes = 0 + for (const file of files) { + if ( + !Buffer.isBuffer(file.buffer) || + typeof file.name !== 'string' || + !file.name.trim() || + typeof file.mimeType !== 'string' || + !file.mimeType.trim() + ) { + throw new Error('Tool file output requires a buffer, filename, and MIME type') + } + assertKnownSizeWithinLimit(file.buffer.length, MAX_BUFFERED_TRANSFER_BYTES, 'Tool output file') + totalBytes += file.buffer.length + assertKnownSizeWithinLimit(totalBytes, MAX_BUFFERED_TRANSFER_BYTES, 'Tool output files') + } + return files +} + +/** Rollback must finish even when the operation's signal is already aborted. */ +async function rollbackCreatedFiles(files: readonly CreatedFile[]): Promise { + for (const file of files) { + try { + await deleteFile(file) + await deleteFileMetadata(file.key) + } catch (error) { + logger.error('Failed to roll back an unpublished tool output file', { + key: file.key, + context: file.context, + error: getErrorMessage(error), + }) + } + } +} + +/** Stores file results and rolls back their objects if final presentation fails. */ +export async function storeInternalToolFileResult( + result: InternalToolFileResult, + context: InternalToolOperationContext, + finalize: (body: unknown) => T, + signal?: AbortSignal +): Promise { + signal?.throwIfAborted() + const files = validateFiles(result) + const scope = resolveFileStorageScope(context) + const createdFiles: CreatedFile[] = [] + const storedFiles = new Map() + + try { + for (const file of files) { + signal?.throwIfAborted() + const metadata = resolveStoredFileMetadata(file.name, file.mimeType, file.buffer) + const storedFile = + scope.kind === 'execution' + ? await uploadExecutionFile( + scope.context, + file.buffer, + metadata.fileName, + metadata.mimeType, + scope.userId + ) + : await uploadCopilotFile({ + buffer: file.buffer, + fileName: metadata.fileName, + contentType: metadata.mimeType, + userId: scope.userId, + }) + createdFiles.push({ key: storedFile.key, context: scope.kind }) + storedFiles.set(file, 'mimeType' in storedFile ? omit(storedFile, ['mimeType']) : storedFile) + signal?.throwIfAborted() + } + const presentedFiles = result.files.map((file) => { + const storedFile = storedFiles.get(file) + if (!storedFile) throw new Error('Tool output file was not stored') + return storedFile + }) + const output = await finalize(result.present(presentedFiles)) + signal?.throwIfAborted() + return output + } catch (error) { + await rollbackCreatedFiles(createdFiles) + signal?.throwIfAborted() + throw error + } +} + +/** Stores trusted in-process file results before their small JSON envelope crosses transport. */ +export async function presentInternalToolOperationResult( + result: InternalToolOperationResult, + context: InternalToolOperationContext, + signal?: AbortSignal +): Promise { + if (result instanceof Response) return result + return storeInternalToolFileResult( + result, + context, + (presented) => { + const body = JSON.stringify(presented) + if (body === undefined) throw new TypeError('Tool file result must be JSON serializable') + assertKnownSizeWithinLimit( + Buffer.byteLength(body, 'utf8'), + MAX_TOOL_RESPONSE_BODY_BYTES, + 'Tool response body' + ) + const headers = new Headers(result.init?.headers) + headers.delete('content-length') + headers.delete('content-encoding') + headers.set('content-type', 'application/json') + return new Response(body, { ...result.init, headers }) + }, + signal + ) +} diff --git a/apps/sim/lib/internal/tool-operations/file-result.ts b/apps/sim/lib/internal/tool-operations/file-result.ts new file mode 100644 index 00000000000..42bb5167891 --- /dev/null +++ b/apps/sim/lib/internal/tool-operations/file-result.ts @@ -0,0 +1,45 @@ +import type { UserFile } from '@/executor/types' + +/** Binary output kept in process until the executor persists it. */ +export interface InternalToolFile { + buffer: Buffer + name: string + mimeType: string +} + +/** The presenter receives stored descriptors, never inline file bytes. */ +export interface InternalToolFileResult { + kind: 'file-output' + files: readonly InternalToolFile[] + present: (files: readonly UserFile[]) => unknown + init?: ResponseInit +} + +export function createInternalToolFilesResult( + files: readonly InternalToolFile[], + present: InternalToolFileResult['present'], + init?: ResponseInit +): InternalToolFileResult { + return { kind: 'file-output', files, present, ...(init ? { init } : {}) } +} + +export function createInternalToolFileResult( + file: InternalToolFile, + present: (file: UserFile) => unknown, + init?: ResponseInit +): InternalToolFileResult { + return createInternalToolFilesResult([file], (files) => present(files[0]!), init) +} + +export function isInternalToolFileResult(value: unknown): value is InternalToolFileResult { + return ( + typeof value === 'object' && + value !== null && + 'kind' in value && + value.kind === 'file-output' && + 'files' in value && + Array.isArray(value.files) && + 'present' in value && + typeof value.present === 'function' + ) +} diff --git a/apps/sim/lib/internal/tool-operations/registry.server.ts b/apps/sim/lib/internal/tool-operations/registry.server.ts index bc8fcf7b7f0..ea568ff50dd 100644 --- a/apps/sim/lib/internal/tool-operations/registry.server.ts +++ b/apps/sim/lib/internal/tool-operations/registry.server.ts @@ -1,7 +1,12 @@ -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' import { isMcpTool } from '@/executor/constants' -type InternalToolOperationHandlerLoader = () => Promise +type InternalToolOperationHandlerLoader = () => Promise< + InternalToolOperationHandler +> const STS_TOOL_IDS = [ 'sts_assume_role', @@ -456,6 +461,7 @@ const JUPYTER_TOOL_IDS = [ 'jupyter_delete_content', 'jupyter_delete_session', 'jupyter_get_content', + 'jupyter_get_content_v2', 'jupyter_interrupt_kernel', 'jupyter_list_contents', 'jupyter_list_kernels', @@ -730,6 +736,7 @@ const OUTLOOK_TOOL_IDS = [ 'outlook_copy', 'outlook_delete', 'outlook_draft', + 'outlook_get_attachment', 'outlook_mark_read', 'outlook_mark_unread', 'outlook_move', @@ -742,6 +749,7 @@ const SSH_TOOL_IDS = [ 'ssh_create_directory', 'ssh_delete_file', 'ssh_download_file', + 'ssh_download_file_v2', 'ssh_execute_command', 'ssh_execute_script', 'ssh_get_system_info', @@ -1012,6 +1020,7 @@ const CURSOR_TOOL_IDS = ['cursor_download_artifact', 'cursor_download_artifact_v const SFTP_TOOL_IDS = [ 'sftp_delete', 'sftp_download', + 'sftp_download_v2', 'sftp_list', 'sftp_mkdir', 'sftp_upload', @@ -1069,7 +1078,12 @@ const PERSONA_TOOL_IDS = ['persona_import_accounts'] as const const SHAREPOINT_TOOL_IDS = ['sharepoint_download_file', 'sharepoint_upload_file'] as const -const QUIVER_TOOL_IDS = ['quiver_text_to_svg', 'quiver_image_to_svg'] as const +const QUIVER_TOOL_IDS = [ + 'quiver_text_to_svg', + 'quiver_image_to_svg', + 'quiver_text_to_svg_v2', + 'quiver_image_to_svg_v2', +] as const const TELEGRAM_TOOL_IDS = ['telegram_send_document'] as const @@ -1787,7 +1801,7 @@ export function getRegisteredInternalToolOperationIds(): string[] { export async function getInternalToolOperationHandler( toolId: string -): Promise { +): Promise | null> { const loader = handlerLoaders.get(toolId) if (loader) return loader() if (isMcpTool(toolId)) { diff --git a/apps/sim/lib/internal/tool-operations/response-limits.ts b/apps/sim/lib/internal/tool-operations/response-limits.ts new file mode 100644 index 00000000000..fb8828b4ef1 --- /dev/null +++ b/apps/sim/lib/internal/tool-operations/response-limits.ts @@ -0,0 +1,2 @@ +/** Maximum inline tool response size; binary file outputs use stored descriptors. */ +export const MAX_TOOL_RESPONSE_BODY_BYTES = 10 * 1024 * 1024 diff --git a/apps/sim/lib/internal/tool-operations/types.ts b/apps/sim/lib/internal/tool-operations/types.ts index 2e1f52d3584..803c299741e 100644 --- a/apps/sim/lib/internal/tool-operations/types.ts +++ b/apps/sim/lib/internal/tool-operations/types.ts @@ -1,4 +1,5 @@ import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution' +import type { InternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import type { ExecutorDelegationOrigin } from '@/executor/types' import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' import type { ToolResponse } from '@/tools/types' @@ -42,4 +43,8 @@ export interface InternalToolOperationCall { signal?: AbortSignal } -export type InternalToolOperationHandler = (request: InternalToolOperationCall) => Promise +export type InternalToolOperationResult = Response | InternalToolFileResult + +export type InternalToolOperationHandler = ( + request: InternalToolOperationCall +) => Promise diff --git a/apps/sim/lib/internal/twilio-voice/execute-tool.ts b/apps/sim/lib/internal/twilio-voice/execute-tool.ts index 8b9a555b9f4..840eedfea9a 100644 --- a/apps/sim/lib/internal/twilio-voice/execute-tool.ts +++ b/apps/sim/lib/internal/twilio-voice/execute-tool.ts @@ -1,7 +1,11 @@ import { getErrorMessage } from '@sim/utils/errors' import { z } from 'zod' import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' import { TwilioVoiceOperationError } from '@/lib/internal/twilio-voice/errors' import { getTwilioRecording } from '@/lib/internal/twilio-voice/operations' @@ -11,7 +15,9 @@ const inputSchema = z.object({ recordingSid: z.string().min(1, 'Recording SID is required'), }) -export const executeTwilioVoiceTool: InternalToolOperationHandler = async (request) => { +export const executeTwilioVoiceTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() if (request.toolId !== 'twilio_voice_get_recording') { return Response.json( @@ -24,12 +30,11 @@ export const executeTwilioVoiceTool: InternalToolOperationHandler = async (reque return Response.json({ success: false, error: 'Invalid request data' }, { status: 400 }) } try { - return Response.json( - await getTwilioRecording(parsed.data, { - requestId: request.requestId, - signal: request.signal, - }) - ) + const result = await getTwilioRecording(parsed.data, { + requestId: request.requestId, + signal: request.signal, + }) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() const status = isPayloadSizeLimitError(error) diff --git a/apps/sim/lib/internal/twilio-voice/operations.test.ts b/apps/sim/lib/internal/twilio-voice/operations.test.ts index dd952e46dbb..8420edeb5cc 100644 --- a/apps/sim/lib/internal/twilio-voice/operations.test.ts +++ b/apps/sim/lib/internal/twilio-voice/operations.test.ts @@ -1,7 +1,12 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { assert, beforeEach, describe, expect, it, vi } from 'vitest' +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ secureFetchWithPinnedIP: vi.fn(), @@ -56,12 +61,23 @@ describe('getTwilioRecording', () => { signal: controller.signal, }) ) - expect(result.output).toEqual( - expect.objectContaining({ - duration: 42, - transcriptionText: 'hello', - file: expect.objectContaining({ name: 'RE123.mp3', data: 'AQID', size: 3 }), - }) - ) + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { name: 'RE123.mp3', mimeType: 'audio/mpeg', buffer: Buffer.from([1, 2, 3]) }, + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', + name: 'RE123.mp3', + type: 'audio/mpeg', + mimeType: 'audio/mpeg', + size: 3, + context: 'execution', + } + expect(result.present([storedFile])).toMatchObject({ + success: true, + output: { duration: 42, transcriptionText: 'hello', file: storedFile }, + }) }) }) diff --git a/apps/sim/lib/internal/twilio-voice/operations.ts b/apps/sim/lib/internal/twilio-voice/operations.ts index 7acc77c44e8..bc551fbe692 100644 --- a/apps/sim/lib/internal/twilio-voice/operations.ts +++ b/apps/sim/lib/internal/twilio-voice/operations.ts @@ -8,10 +8,14 @@ import { readResponseJsonWithLimit, readResponseToBufferWithLimit, } from '@/lib/core/utils/stream-limits' +import { + createInternalToolFileResult, + type InternalToolFile, +} from '@/lib/internal/tool-operations/file-result' import { TwilioVoiceOperationError } from '@/lib/internal/twilio-voice/errors' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { getExtensionFromMimeType } from '@/lib/uploads/utils/file-utils' -import type { TwilioGetRecordingOutput, TwilioGetRecordingParams } from '@/tools/twilio_voice/types' +import type { TwilioGetRecordingParams } from '@/tools/twilio_voice/types' const logger = createLogger('TwilioGetRecordingOperation') const MAX_TWILIO_JSON_BYTES = 2 * 1024 * 1024 @@ -67,7 +71,7 @@ async function fetchPinned( export async function getTwilioRecording( input: TwilioGetRecordingParams, context: TwilioVoiceOperationContext -): Promise { +) { context.signal?.throwIfAborted() if (!input.accountSid.startsWith('AC')) { throw new TwilioVoiceOperationError( @@ -131,7 +135,7 @@ export async function getTwilioRecording( logger.warn('Failed to fetch Twilio transcription', { requestId: context.requestId, error }) } - let file: TwilioGetRecordingOutput['output']['file'] + let file: InternalToolFile | undefined if (mediaUrl) { try { const response = await fetchPinned( @@ -151,8 +155,7 @@ export async function getTwilioRecording( file = { name: `${data.sid || input.recordingSid}.${getExtensionFromMimeType(mimeType) || 'dat'}`, mimeType, - data: buffer.toString('base64'), - size: buffer.length, + buffer, } } } catch (error) { @@ -164,25 +167,26 @@ export async function getTwilioRecording( } } - return { + const output = { success: true, - output: { - success: true, - recordingSid: data.sid, - callSid: data.call_sid, - duration: data.duration ? Number.parseInt(data.duration, 10) : undefined, - status: data.status, - channels: data.channels, - source: data.source, - mediaUrl, - file, - price: data.price, - priceUnit: data.price_unit, - uri: data.uri, - transcriptionText: transcription?.transcription_text, - transcriptionStatus: transcription?.status, - transcriptionPrice: transcription?.price, - transcriptionPriceUnit: transcription?.price_unit, - }, + recordingSid: data.sid, + callSid: data.call_sid, + duration: data.duration ? Number.parseInt(data.duration, 10) : undefined, + status: data.status, + channels: data.channels, + source: data.source, + mediaUrl, + price: data.price, + priceUnit: data.price_unit, + uri: data.uri, + transcriptionText: transcription?.transcription_text, + transcriptionStatus: transcription?.status, + transcriptionPrice: transcription?.price, + transcriptionPriceUnit: transcription?.price_unit, } + if (!file) return { success: true, output } + return createInternalToolFileResult(file, (storedFile) => ({ + success: true, + output: { ...output, file: storedFile }, + })) } diff --git a/apps/sim/lib/internal/vanta/execute-tool.ts b/apps/sim/lib/internal/vanta/execute-tool.ts index b510d7453ae..26181300c4b 100644 --- a/apps/sim/lib/internal/vanta/execute-tool.ts +++ b/apps/sim/lib/internal/vanta/execute-tool.ts @@ -1,5 +1,9 @@ import { getErrorMessage } from '@sim/utils/errors' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' import { VantaOperationError } from '@/lib/internal/vanta/errors' import { vantaDownloadDocumentFileInputSchema, @@ -13,7 +17,9 @@ import { import { vantaQueryBodySchema } from '@/lib/internal/vanta/schema' /** Executes the Vanta tool family without a same-origin HTTP hop. */ -export const executeVantaTool: InternalToolOperationHandler = async (request) => { +export const executeVantaTool: InternalToolOperationHandler = async ( + request +) => { request.signal?.throwIfAborted() const schema = request.toolId === 'vanta_upload_document_file' @@ -47,7 +53,7 @@ export const executeVantaTool: InternalToolOperationHandler = async (request) => context ) request.signal?.throwIfAborted() - return Response.json(result) + return isInternalToolFileResult(result) ? result : Response.json(result) } const query = vantaQueryBodySchema.parse(parsed.data) diff --git a/apps/sim/lib/internal/vanta/operations.test.ts b/apps/sim/lib/internal/vanta/operations.test.ts index 732f6561972..df66cd149cc 100644 --- a/apps/sim/lib/internal/vanta/operations.test.ts +++ b/apps/sim/lib/internal/vanta/operations.test.ts @@ -1,7 +1,12 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { assert, beforeEach, describe, expect, it, vi } from 'vitest' +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' const mocks = vi.hoisted(() => ({ fetchAuth: vi.fn(), @@ -98,19 +103,23 @@ describe('Vanta operations', () => { ) expect(mocks.fetchAuth.mock.calls[0]?.[2]).toEqual({ signal: context.signal }) - expect(result).toEqual({ + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { name: 'report final.pdf', mimeType: 'application/pdf', buffer: Buffer.from('hello') }, + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', + name: 'report final.pdf', + type: 'application/pdf', + mimeType: 'application/pdf', + size: 5, + context: 'execution', + } + expect(result.present([storedFile])).toEqual({ success: true, - output: { - file: { - name: 'report final.pdf', - mimeType: 'application/pdf', - data: Buffer.from('hello').toString('base64'), - size: 5, - }, - name: 'report final.pdf', - mimeType: 'application/pdf', - size: 5, - }, + output: { file: storedFile, name: 'report final.pdf', mimeType: 'application/pdf', size: 5 }, }) }) diff --git a/apps/sim/lib/internal/vanta/operations.ts b/apps/sim/lib/internal/vanta/operations.ts index ced9bece5cd..c649d9b53af 100644 --- a/apps/sim/lib/internal/vanta/operations.ts +++ b/apps/sim/lib/internal/vanta/operations.ts @@ -4,6 +4,10 @@ import { readResponseJsonWithLimit, readResponseToBufferWithLimit, } from '@/lib/core/utils/stream-limits' +import { + createInternalToolFileResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import { fetchVantaWithAuth, getVantaBaseUrl, @@ -511,7 +515,7 @@ export async function executeVantaUploadDocumentFile( export async function executeVantaDownloadDocumentFile( input: VantaDownloadDocumentFileInput, context: VantaFileOperationContext -): Promise> { +): Promise { context.signal?.throwIfAborted() const mediaUrl = buildVantaUrl( getVantaBaseUrl(input.region), @@ -565,13 +569,8 @@ export async function executeVantaDownloadDocumentFile( const name = fileNameFromContentDisposition(response.headers.get('content-disposition')) || `vanta-document-file-${input.uploadedFileId}` - return { + return createInternalToolFileResult({ buffer, name, mimeType }, (file) => ({ success: true, - output: { - file: { name, mimeType, data: buffer.toString('base64'), size: buffer.length }, - name, - mimeType, - size: buffer.length, - }, - } + output: { file, name, mimeType, size: file.size }, + })) } diff --git a/apps/sim/lib/internal/zoho-desk/execute-tool.test.ts b/apps/sim/lib/internal/zoho-desk/execute-tool.test.ts index 907d649af75..126da373975 100644 --- a/apps/sim/lib/internal/zoho-desk/execute-tool.test.ts +++ b/apps/sim/lib/internal/zoho-desk/execute-tool.test.ts @@ -3,12 +3,14 @@ */ import { createExecutionContext } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { createInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' const mocks = vi.hoisted(() => ({ getZohoDeskAttachment: vi.fn() })) vi.mock('@/lib/internal/zoho-desk/operations', () => ({ getZohoDeskAttachment: mocks.getZohoDeskAttachment, - MAX_ZOHO_DESK_ATTACHMENT_BYTES: 7 * 1024 * 1024, })) import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' @@ -33,28 +35,46 @@ function request(overrides: Partial = {}): InternalTo describe('executeZohoDeskTool', () => { beforeEach(() => { vi.clearAllMocks() - mocks.getZohoDeskAttachment.mockResolvedValue({ - success: true, - output: { file: { name: 'file.pdf', mimeType: 'application/pdf', data: 'YQ==' } }, - }) }) it('dispatches the typed operation with cancellation', async () => { const controller = new AbortController() - const response = await executeZohoDeskTool(request({ signal: controller.signal })) + const result = createInternalToolFileResult( + { buffer: Buffer.alloc(12 * 1024 * 1024), name: 'file.pdf', mimeType: 'application/pdf' }, + (file) => ({ success: true, output: { file } }) + ) + mocks.getZohoDeskAttachment.mockResolvedValue(result) - expect(response.status).toBe(200) + expect(await executeZohoDeskTool(request({ signal: controller.signal }))).toBe(result) expect(mocks.getZohoDeskAttachment).toHaveBeenCalledWith( expect.objectContaining({ orgId: 'org-1' }), { signal: controller.signal } ) }) + it('projects the buffered file limit as 413', async () => { + mocks.getZohoDeskAttachment.mockRejectedValue( + new PayloadSizeLimitError({ + label: 'Zoho Desk attachment', + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + observedBytes: MAX_BUFFERED_TRANSFER_BYTES + 1, + }) + ) + const response = await executeZohoDeskTool(request()) + if (!(response instanceof Response)) throw new Error('Expected an error response') + expect(response.status).toBe(413) + await expect(response.json()).resolves.toEqual({ + success: false, + error: 'Attachment exceeds the 100 MB download limit', + }) + }) + it('preserves operation status', async () => { mocks.getZohoDeskAttachment.mockRejectedValue( new ZohoDeskOperationError('Invalid attachment href', 400) ) const response = await executeZohoDeskTool(request()) + if (!(response instanceof Response)) throw new Error('Expected an error response') expect(response.status).toBe(400) }) }) diff --git a/apps/sim/lib/internal/zoho-desk/execute-tool.ts b/apps/sim/lib/internal/zoho-desk/execute-tool.ts index 0af245bbe06..1a99f24ba21 100644 --- a/apps/sim/lib/internal/zoho-desk/execute-tool.ts +++ b/apps/sim/lib/internal/zoho-desk/execute-tool.ts @@ -1,12 +1,13 @@ import { getErrorMessage } from '@sim/utils/errors' import { z } from 'zod' import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' import { ZohoDeskOperationError } from '@/lib/internal/zoho-desk/errors' -import { - getZohoDeskAttachment, - MAX_ZOHO_DESK_ATTACHMENT_BYTES, -} from '@/lib/internal/zoho-desk/operations' +import { getZohoDeskAttachment } from '@/lib/internal/zoho-desk/operations' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' const inputSchema = z.object({ accessToken: z.string().min(1), @@ -16,7 +17,9 @@ const inputSchema = z.object({ fileName: z.string().optional(), }) -export const executeZohoDeskTool: InternalToolOperationHandler = async (request) => { +export const executeZohoDeskTool: InternalToolOperationHandler< + InternalToolOperationResult +> = async (request) => { request.signal?.throwIfAborted() if (request.toolId !== 'zoho_desk_get_attachment') { return Response.json( @@ -29,18 +32,14 @@ export const executeZohoDeskTool: InternalToolOperationHandler = async (request) return Response.json({ success: false, error: 'Invalid request data' }, { status: 400 }) } try { - return Response.json( - await getZohoDeskAttachment(parsed.data, { - signal: request.signal, - }) - ) + return await getZohoDeskAttachment(parsed.data, { signal: request.signal }) } catch (error) { request.signal?.throwIfAborted() if (isPayloadSizeLimitError(error)) { return Response.json( { success: false, - error: `Attachment exceeds the ${Math.floor(MAX_ZOHO_DESK_ATTACHMENT_BYTES / (1024 * 1024))} MB download limit`, + error: `Attachment exceeds the ${Math.floor(MAX_BUFFERED_TRANSFER_BYTES / (1024 * 1024))} MB download limit`, }, { status: 413 } ) diff --git a/apps/sim/lib/internal/zoho-desk/operations.test.ts b/apps/sim/lib/internal/zoho-desk/operations.test.ts new file mode 100644 index 00000000000..5283ee5c1e6 --- /dev/null +++ b/apps/sim/lib/internal/zoho-desk/operations.test.ts @@ -0,0 +1,146 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' + +const mocks = vi.hoisted(() => ({ secureFetchWithValidation: vi.fn() })) + +vi.mock('@/lib/core/security/input-validation.server', () => ({ + secureFetchWithValidation: mocks.secureFetchWithValidation, +})) + +import { getZohoDeskAttachment } from '@/lib/internal/zoho-desk/operations' + +const input = { + accessToken: 'token', + orgId: 'org-1', + href: '/api/v1/tickets/1/attachments/2/content', + apiDomain: 'https://desk.zoho.eu', +} + +describe('getZohoDeskAttachment', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns a 12 MiB attachment for central storage while preserving download guards', async () => { + const buffer = Buffer.alloc(12 * 1024 * 1024, 42) + const controller = new AbortController() + mocks.secureFetchWithValidation.mockResolvedValue( + new Response(new Uint8Array(buffer), { + headers: { + 'content-type': 'application/pdf', + 'content-disposition': "attachment; filename*=UTF-8''ticket%20attachment.pdf", + }, + }) + ) + + const result = await getZohoDeskAttachment(input, { signal: controller.signal }) + + expect(mocks.secureFetchWithValidation).toHaveBeenCalledWith( + 'https://desk.zoho.eu/api/v1/tickets/1/attachments/2/content', + { + profile: 'contentFetch', + method: 'GET', + headers: { + Authorization: 'Zoho-oauthtoken token', + orgId: 'org-1', + 'Content-Type': 'application/json', + }, + timeout: 30_000, + maxResponseBytes: MAX_BUFFERED_TRANSFER_BYTES, + stripAuthOnRedirect: true, + signal: controller.signal, + } + ) + expect(result.files).toHaveLength(1) + expect(result.files[0]?.name).toBe('ticket attachment.pdf') + expect(result.files[0]?.mimeType).toBe('application/pdf') + expect(result.files[0]?.buffer.equals(buffer)).toBe(true) + const file = { + id: 'file-1', + name: 'ticket attachment.pdf', + key: 'execution/workspace/workflow/run/file.pdf', + url: '/api/files/file-1', + type: 'application/pdf', + mimeType: 'application/pdf', + size: buffer.length, + context: 'execution', + } + const presented = result.present([file]) + expect(presented).toEqual({ success: true, output: { file } }) + expect(presented).not.toHaveProperty('output.file.data') + expect(JSON.stringify(presented).length).toBeLessThan(1024) + }) + + it('rejects a declared attachment size above the buffered transfer limit', async () => { + mocks.secureFetchWithValidation.mockResolvedValue( + new Response(new Uint8Array(), { + headers: { 'content-length': String(MAX_BUFFERED_TRANSFER_BYTES + 1) }, + }) + ) + + await expect(getZohoDeskAttachment(input, {})).rejects.toMatchObject({ + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + observedBytes: MAX_BUFFERED_TRANSFER_BYTES + 1, + }) + }) + + it('rejects an oversized stream even without a content-length header', async () => { + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(MAX_BUFFERED_TRANSFER_BYTES + 1)) + controller.close() + }, + }) + mocks.secureFetchWithValidation.mockResolvedValue(new Response(body)) + + await expect(getZohoDeskAttachment(input, {})).rejects.toMatchObject({ + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + observedBytes: MAX_BUFFERED_TRANSFER_BYTES + 1, + }) + }) + + it('retains filename overrides and the binary MIME fallback for empty files', async () => { + mocks.secureFetchWithValidation.mockResolvedValue(new Response(new Uint8Array())) + + const result = await getZohoDeskAttachment({ ...input, fileName: ' empty.bin ' }, {}) + + expect(result.files[0]?.name).toBe('empty.bin') + expect(result.files[0]?.mimeType).toBe('application/octet-stream') + expect(result.files[0]?.buffer.length).toBe(0) + }) + + it.each(['https://desk.zoho.com.attacker.example/attachment', 'http://desk.zoho.com/attachment'])( + 'rejects untrusted attachment URL %s before sending credentials', + async (href) => { + await expect(getZohoDeskAttachment({ ...input, href }, {})).rejects.toMatchObject({ + status: 400, + }) + expect(mocks.secureFetchWithValidation).not.toHaveBeenCalled() + } + ) + + it.each([ + [403, 403], + [500, 502], + [204, 502], + ])('preserves provider HTTP %i as operation status %i', async (status, expectedStatus) => { + mocks.secureFetchWithValidation.mockResolvedValue(new Response(null, { status })) + + await expect(getZohoDeskAttachment(input, {})).rejects.toMatchObject({ + status: expectedStatus, + }) + }) + + it('does not start a download after cancellation', async () => { + const controller = new AbortController() + controller.abort(new Error('Execution cancelled')) + + await expect(getZohoDeskAttachment(input, { signal: controller.signal })).rejects.toThrow( + 'Execution cancelled' + ) + expect(mocks.secureFetchWithValidation).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/internal/zoho-desk/operations.ts b/apps/sim/lib/internal/zoho-desk/operations.ts index 8cb3f45a30e..c225cb87ea0 100644 --- a/apps/sim/lib/internal/zoho-desk/operations.ts +++ b/apps/sim/lib/internal/zoho-desk/operations.ts @@ -1,5 +1,11 @@ import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { readResponseToBufferWithLimit } from '@/lib/core/utils/stream-limits' +import { + createInternalToolFileResult, + type InternalToolFileResult, +} from '@/lib/internal/tool-operations/file-result' import { ZohoDeskOperationError } from '@/lib/internal/zoho-desk/errors' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { isZohoHost } from '@/tools/zoho_desk/host-allowlist' import type { ZohoDeskGetAttachmentParams } from '@/tools/zoho_desk/types' import { @@ -9,8 +15,6 @@ import { resolveZohoAttachmentUrl, } from '@/tools/zoho_desk/utils' -export const MAX_ZOHO_DESK_ATTACHMENT_BYTES = 7 * 1024 * 1024 - export interface ZohoDeskOperationContext { signal?: AbortSignal } @@ -18,10 +22,7 @@ export interface ZohoDeskOperationContext { export async function getZohoDeskAttachment( input: ZohoDeskGetAttachmentParams, context: ZohoDeskOperationContext -): Promise<{ - success: true - output: { file: { data: string; mimeType: string; name: string } } -}> { +): Promise { context.signal?.throwIfAborted() let downloadUrl: URL try { @@ -41,7 +42,7 @@ export async function getZohoDeskAttachment( method: 'GET', headers: buildZohoDeskHeaders({ accessToken: input.accessToken, orgId: input.orgId }), timeout: 30_000, - maxResponseBytes: MAX_ZOHO_DESK_ATTACHMENT_BYTES, + maxResponseBytes: MAX_BUFFERED_TRANSFER_BYTES, stripAuthOnRedirect: true, signal: context.signal, }) @@ -57,20 +58,22 @@ export async function getZohoDeskAttachment( 502 ) } - const buffer = Buffer.from(await response.arrayBuffer()) + const buffer = await readResponseToBufferWithLimit(response, { + maxBytes: MAX_BUFFERED_TRANSFER_BYTES, + label: 'Zoho Desk attachment', + signal: context.signal, + }) context.signal?.throwIfAborted() - return { - success: true, - output: { - file: { - data: buffer.toString('base64'), - mimeType: response.headers.get('content-type') || 'application/octet-stream', - name: deriveAttachmentName( - input.fileName, - response.headers.get('content-disposition'), - downloadUrl.pathname - ), - }, + return createInternalToolFileResult( + { + buffer, + mimeType: response.headers.get('content-type') || 'application/octet-stream', + name: deriveAttachmentName( + input.fileName, + response.headers.get('content-disposition'), + downloadUrl.pathname + ), }, - } + (file) => ({ success: true, output: { file } }) + ) } diff --git a/apps/sim/lib/internal/zoom/execute-tool.ts b/apps/sim/lib/internal/zoom/execute-tool.ts index 803acb22ac3..49e56cc9a72 100644 --- a/apps/sim/lib/internal/zoom/execute-tool.ts +++ b/apps/sim/lib/internal/zoom/execute-tool.ts @@ -1,6 +1,10 @@ import { getErrorMessage } from '@sim/utils/errors' import { z } from 'zod' -import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' +import type { + InternalToolOperationHandler, + InternalToolOperationResult, +} from '@/lib/internal/tool-operations/types' import { ZoomOperationError } from '@/lib/internal/zoom/errors' import { getZoomMeetingRecordings } from '@/lib/internal/zoom/operations' @@ -12,7 +16,9 @@ const inputSchema = z.object({ downloadFiles: z.boolean().default(false), }) -export const executeZoomTool: InternalToolOperationHandler = async (request) => { +export const executeZoomTool: InternalToolOperationHandler = async ( + request +) => { request.signal?.throwIfAborted() if (request.toolId !== 'zoom_get_meeting_recordings') { return Response.json( @@ -25,12 +31,11 @@ export const executeZoomTool: InternalToolOperationHandler = async (request) => return Response.json({ success: false, error: 'Invalid request data' }, { status: 400 }) } try { - return Response.json( - await getZoomMeetingRecordings(parsed.data, { - requestId: request.requestId, - signal: request.signal, - }) - ) + const result = await getZoomMeetingRecordings(parsed.data, { + requestId: request.requestId, + signal: request.signal, + }) + return isInternalToolFileResult(result) ? result : Response.json(result) } catch (error) { request.signal?.throwIfAborted() if (error instanceof ZoomOperationError) { diff --git a/apps/sim/lib/internal/zoom/operations.test.ts b/apps/sim/lib/internal/zoom/operations.test.ts index d21429d74d8..f56d9a69a94 100644 --- a/apps/sim/lib/internal/zoom/operations.test.ts +++ b/apps/sim/lib/internal/zoom/operations.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { assert, beforeEach, describe, expect, it, vi } from 'vitest' import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' const mocks = vi.hoisted(() => ({ @@ -16,6 +16,10 @@ vi.mock('@/lib/core/security/input-validation.server', () => ({ vi.mock('@/lib/uploads/shared/types', () => ({ MAX_BUFFERED_TRANSFER_BYTES: 5 })) +import { + isInternalToolFileResult, + type StoredToolFile, +} from '@/lib/internal/tool-operations/file-result' import { getZoomMeetingRecordings } from '@/lib/internal/zoom/operations' describe('getZoomMeetingRecordings', () => { @@ -24,6 +28,44 @@ describe('getZoomMeetingRecordings', () => { mocks.validateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.1' }) }) + it('returns stored recording references with the original recording metadata', async () => { + mocks.secureFetchWithPinnedIP + .mockResolvedValueOnce( + Response.json({ + id: 'meeting-1', + recording_files: [{ id: 'one', download_url: 'https://files.example/one' }], + }) + ) + .mockResolvedValueOnce(new Response('one', { headers: { 'content-type': 'video/mp4' } })) + + const result = await getZoomMeetingRecordings( + { accessToken: 'token', meetingId: 'meeting-1', downloadFiles: true }, + { requestId: 'request-1' } + ) + + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { name: 'zoom-recording-one.mp4', mimeType: 'video/mp4', buffer: Buffer.from('one') }, + ]) + const storedFile: StoredToolFile = { + id: 'stored-file-1', + key: 'execution/stored-file-1', + url: '/api/files/serve/stored-file-1', + name: 'zoom-recording-one.mp4', + type: 'video/mp4', + mimeType: 'video/mp4', + size: 3, + context: 'execution', + } + expect(result.present([storedFile])).toMatchObject({ + success: true, + output: { + recording: { id: 'meeting-1', recording_files: [{ id: 'one' }] }, + files: [storedFile], + }, + }) + }) + it('downloads sequentially and rejects cumulative recording bytes', async () => { mocks.secureFetchWithPinnedIP .mockResolvedValueOnce( diff --git a/apps/sim/lib/internal/zoom/operations.ts b/apps/sim/lib/internal/zoom/operations.ts index 1860b06e667..72be9339db7 100644 --- a/apps/sim/lib/internal/zoom/operations.ts +++ b/apps/sim/lib/internal/zoom/operations.ts @@ -4,6 +4,10 @@ import { validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { + createInternalToolFilesResult, + type InternalToolFile, +} from '@/lib/internal/tool-operations/file-result' import { ZoomOperationError } from '@/lib/internal/zoom/errors' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' import { getExtensionFromMimeType } from '@/lib/uploads/utils/file-utils' @@ -52,13 +56,7 @@ export interface ZoomOperationContext { export async function getZoomMeetingRecordings( input: ZoomGetMeetingRecordingsParams, context: ZoomOperationContext -): Promise<{ - success: true - output: { - recording: ZoomRecordingsResponse & { recording_files: ZoomRecordingFile[] } - files?: Array<{ name: string; mimeType: string; data: string; size: number }> - } -}> { +) { context.signal?.throwIfAborted() const query = new URLSearchParams() if (input.includeFolderItems != null) { @@ -87,7 +85,7 @@ export async function getZoomMeetingRecordings( throw new ZoomOperationError(errorData.message || `Zoom API error: ${response.status}`, 400) } const data = (await response.json()) as ZoomRecordingsResponse - const files: Array<{ name: string; mimeType: string; data: string; size: number }> = [] + const files: InternalToolFile[] = [] let bufferedBytes = 0 if (input.downloadFiles && Array.isArray(data.recording_files)) { @@ -134,8 +132,7 @@ export async function getZoomMeetingRecordings( files.push({ name: `zoom-recording-${file.id || file.recording_start || Date.now()}.${extension}`, mimeType, - data: buffer.toString('base64'), - size: buffer.length, + buffer, }) } catch (error) { context.signal?.throwIfAborted() @@ -153,36 +150,35 @@ export async function getZoomMeetingRecordings( } } - return { - success: true, - output: { - recording: { - uuid: data.uuid, - id: data.id, - account_id: data.account_id, - host_id: data.host_id, - topic: data.topic, - type: data.type, - start_time: data.start_time, - duration: data.duration, - total_size: data.total_size, - recording_count: data.recording_count, - share_url: data.share_url, - recording_files: (data.recording_files || []).map((file) => ({ - id: file.id, - meeting_id: file.meeting_id, - recording_start: file.recording_start, - recording_end: file.recording_end, - file_type: file.file_type, - file_extension: file.file_extension, - file_size: file.file_size, - play_url: file.play_url, - download_url: file.download_url, - status: file.status, - recording_type: file.recording_type, - })), - }, - files: files.length > 0 ? files : undefined, - }, + const recording = { + uuid: data.uuid, + id: data.id, + account_id: data.account_id, + host_id: data.host_id, + topic: data.topic, + type: data.type, + start_time: data.start_time, + duration: data.duration, + total_size: data.total_size, + recording_count: data.recording_count, + share_url: data.share_url, + recording_files: (data.recording_files || []).map((file) => ({ + id: file.id, + meeting_id: file.meeting_id, + recording_start: file.recording_start, + recording_end: file.recording_end, + file_type: file.file_type, + file_extension: file.file_extension, + file_size: file.file_size, + play_url: file.play_url, + download_url: file.download_url, + status: file.status, + recording_type: file.recording_type, + })), } + if (files.length === 0) return { success: true, output: { recording } } + return createInternalToolFilesResult(files, (storedFiles) => ({ + success: true, + output: { recording, files: storedFiles }, + })) } diff --git a/apps/sim/lib/permission-groups/block-successors.generated.ts b/apps/sim/lib/permission-groups/block-successors.generated.ts index d4f7c30c858..6892fa1b402 100644 --- a/apps/sim/lib/permission-groups/block-successors.generated.ts +++ b/apps/sim/lib/permission-groups/block-successors.generated.ts @@ -9,9 +9,12 @@ */ export const BLOCK_ACCESS_SUCCESSORS: Record = { api_trigger: 'start_trigger', + box: 'box_v2', chat_trigger: 'start_trigger', confluence: 'confluence_v2', cursor: 'cursor_v2', + dropbox: 'dropbox_v2', + dub: 'dub_v2', extend: 'extend_v2', file: 'file_v5', file_v2: 'file_v5', @@ -28,20 +31,26 @@ export const BLOCK_ACCESS_SUCCESSORS: Record = { image_generator: 'image_generator_v2', input_trigger: 'start_trigger', intercom: 'intercom_v2', + jupyter: 'jupyter_v2', kalshi: 'kalshi_v2', linear: 'linear_v2', logs: 'logs_v2', manual_trigger: 'start_trigger', + microsoft_dataverse: 'microsoft_dataverse_v2', microsoft_excel: 'microsoft_excel_v2', mistral_parse: 'mistral_parse_v3', mistral_parse_v2: 'mistral_parse_v3', notion: 'notion_v2', openai: 'embeddings', pulse: 'pulse_v2', + quiver: 'quiver_v2', reducto: 'reducto_v2', router: 'router_v2', + servicenow: 'servicenow_v2', + sftp: 'sftp_v2', sharepoint: 'sharepoint_v2', slack: 'slack_v2', + ssh: 'ssh_v2', starter: 'start_trigger', stt: 'stt_v2', table: 'table_v2', diff --git a/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.test.ts b/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.test.ts new file mode 100644 index 00000000000..538bde91cae --- /dev/null +++ b/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.test.ts @@ -0,0 +1,56 @@ +/** @vitest-environment node */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockUploadFile } = vi.hoisted(() => ({ mockUploadFile: vi.fn() })) + +vi.mock('@/lib/uploads/core/storage-service', () => ({ + uploadFile: mockUploadFile, + downloadFile: vi.fn(), +})) +vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'https://sim.example' })) + +import { uploadCopilotFile } from '@/lib/uploads/contexts/copilot/copilot-file-manager' +import type { UploadFileOptions } from '@/lib/uploads/shared/types' + +describe('Copilot output key allocation', () => { + beforeEach(() => { + vi.clearAllMocks() + mockUploadFile.mockImplementation(async (options: UploadFileOptions) => ({ + key: options.customKey, + path: `/api/files/serve/${encodeURIComponent(options.customKey!)}`, + name: options.customKey, + type: options.contentType, + size: options.file.length, + })) + }) + + it('gives concurrent same-named files unique owned keys and preserves their display names', async () => { + const upload = (userId: string) => + uploadCopilotFile({ + buffer: Buffer.from(userId), + fileName: 'report.xlsx', + contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + userId, + }) + const [first, second] = await Promise.all([upload('user-1'), upload('user-2')]) + + expect(first.key).not.toBe(second.key) + expect(first.key).toMatch(/^copilot\/[0-9a-f-]+\/report\.xlsx$/) + for (const stored of [first, second]) { + expect(stored.name).toBe('report.xlsx') + expect(stored.context).toBe('copilot') + expect(stored.url).toBe( + `https://sim.example/api/files/serve/${encodeURIComponent(stored.key)}` + ) + } + for (const [options] of mockUploadFile.mock.calls) { + expect(options.preserveKey).toBe(true) + expect(options.cleanupOnMetadataFailure).toBe(true) + expect(options.fileName).toBe('report.xlsx') + expect(options.metadata.originalName).toBe('report.xlsx') + expect(options.context).toBe('copilot') + } + expect(mockUploadFile.mock.calls[0][0].metadata.userId).toBe('user-1') + expect(mockUploadFile.mock.calls[1][0].metadata.userId).toBe('user-2') + }) +}) diff --git a/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts b/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts index 9f182f09eb3..c6f537a099a 100644 --- a/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/copilot/copilot-file-manager.ts @@ -1,5 +1,7 @@ import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' import { getBaseUrl } from '@/lib/core/utils/urls' +import { buildStorageKeySegment } from '@/lib/uploads/core/storage-key' import { downloadFile, uploadFile } from '@/lib/uploads/core/storage-service' const logger = createLogger('CopilotFileManager') @@ -52,11 +54,15 @@ export async function uploadCopilotFile(options: { contentType: string userId: string }): Promise { + const storageKey = `copilot/${generateId()}/${buildStorageKeySegment('', options.fileName)}` const fileInfo = await uploadFile({ file: options.buffer, fileName: options.fileName, contentType: options.contentType, context: 'copilot', + customKey: storageKey, + preserveKey: true, + cleanupOnMetadataFailure: true, metadata: { userId: options.userId, originalName: options.fileName, @@ -77,7 +83,7 @@ export async function uploadCopilotFile(options: { id: fileInfo.key, key: fileInfo.key, context: 'copilot', - name: fileInfo.name, + name: options.fileName, url, size: fileInfo.size, type: fileInfo.type, diff --git a/apps/sim/lib/uploads/contexts/execution/execution-file-manager.test.ts b/apps/sim/lib/uploads/contexts/execution/execution-file-manager.test.ts index 1d108c60d37..9e4a6b0f7b8 100644 --- a/apps/sim/lib/uploads/contexts/execution/execution-file-manager.test.ts +++ b/apps/sim/lib/uploads/contexts/execution/execution-file-manager.test.ts @@ -167,4 +167,38 @@ describe('uploadExecutionFile key allocation', () => { expect(mockDeleteFromS3).toHaveBeenCalledTimes(1) expect(dbChainMockFns.set).toHaveBeenCalledWith({ deletedAt: expect.any(Date) }) }) + + it('removes the uploaded object and metadata when creating its download URL fails', async () => { + mockGetPresignedUrlWithConfig.mockRejectedValueOnce(new Error('Presigning failed')) + + await expect( + uploadExecutionFile(context, Buffer.from('file'), 'file.txt', 'text/plain', 'user-1') + ).rejects.toThrow('Presigning failed') + + expect(mockDeleteFromS3.mock.calls[0]?.[0]).toBe(mockUploadToS3.mock.calls[0]?.[1]) + expect(dbChainMockFns.update).toHaveBeenCalledTimes(1) + }) + + it('removes its unique object when metadata insertion fails before uploadFile returns', async () => { + dbChainMockFns.returning.mockRejectedValueOnce(new Error('Metadata persistence failed')) + + await expect( + uploadExecutionFile(context, Buffer.from('file'), 'file.txt', 'text/plain', 'user-1') + ).rejects.toThrow('Metadata persistence failed') + + expect(mockDeleteFromS3.mock.calls[0]?.[0]).toBe(mockUploadToS3.mock.calls[0]?.[1]) + expect(mockDeleteFromS3).toHaveBeenCalledOnce() + expect(mockGetPresignedUrlWithConfig).not.toHaveBeenCalled() + }) + + it('keeps the original upload error if cleanup also fails', async () => { + mockGetPresignedUrlWithConfig.mockRejectedValueOnce(new Error('Presigning failed')) + mockDeleteFromS3.mockRejectedValueOnce(new Error('Deletion failed')) + + await expect( + uploadExecutionFile(context, Buffer.from('file'), 'file.txt', 'text/plain', 'user-1') + ).rejects.toThrow('Presigning failed') + + expect(mockDeleteFromS3).toHaveBeenCalledTimes(1) + }) }) diff --git a/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts b/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts index b9bb85213b7..8cef00a6767 100644 --- a/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/execution/execution-file-manager.ts @@ -14,6 +14,7 @@ import { type WorkspaceFileSecretProvenance, } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance' import { + deleteFileMetadata, deleteFileMetadataByIdentity, type FileMetadataRecord, insertImmutableFileMetadata, @@ -123,6 +124,7 @@ export async function uploadExecutionFile( context: 'execution', preserveKey: true, // Don't add timestamp prefix customKey: storageKey, // Use exact execution-scoped key + cleanupOnMetadataFailure: true, metadata, // Pass metadata for cloud storage and database tracking ...(secretProvenance ? { persistMetadata: false } : {}), }) @@ -174,7 +176,7 @@ export async function uploadExecutionFile( }) return userFile } catch (error) { - if (secretProvenance && uploadedKey) { + if (uploadedKey) { try { await StorageService.deleteFile({ key: uploadedKey, context: 'execution' }) if (recordedFile) { @@ -184,9 +186,11 @@ export async function uploadExecutionFile( context: 'execution', contentUpdatedAt: recordedFile.contentUpdatedAt, }) + } else if (!secretProvenance) { + await deleteFileMetadata(uploadedKey) } } catch (cleanupError) { - logger.warn('Could not remove an unreturned execution file', { + logger.error('Failed to clean up an unpublished execution file', { key: uploadedKey, error: getErrorMessage(cleanupError), }) diff --git a/apps/sim/lib/uploads/core/storage-service.local.test.ts b/apps/sim/lib/uploads/core/storage-service.local.test.ts index 230d16086b1..49c9e59a78d 100644 --- a/apps/sim/lib/uploads/core/storage-service.local.test.ts +++ b/apps/sim/lib/uploads/core/storage-service.local.test.ts @@ -6,10 +6,13 @@ import { join } from 'node:path' import { resetDbChainMock } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { testDirectory, mockInsertMetadata } = vi.hoisted(() => ({ - testDirectory: `/tmp/sim-knowledge-upload-compensation-${process.pid}`, - mockInsertMetadata: vi.fn(), -})) +const { testDirectory, mockInsertMetadata, mockInsertFileMetadata, mockDeleteFileMetadata } = + vi.hoisted(() => ({ + testDirectory: `/tmp/sim-knowledge-upload-compensation-${process.pid}`, + mockInsertMetadata: vi.fn(), + mockInsertFileMetadata: vi.fn(), + mockDeleteFileMetadata: vi.fn(), + })) vi.mock('@/lib/uploads/core/setup.server', () => ({ UPLOAD_DIR_SERVER: testDirectory })) vi.mock('@/lib/uploads/config', () => ({ @@ -19,7 +22,8 @@ vi.mock('@/lib/uploads/config', () => ({ getStorageConfig: () => ({}), })) vi.mock('@/lib/uploads/server/metadata', () => ({ - insertFileMetadata: vi.fn(), + insertFileMetadata: mockInsertFileMetadata, + deleteFileMetadata: mockDeleteFileMetadata, insertImmutableFileMetadata: mockInsertMetadata, })) @@ -63,6 +67,8 @@ describe('local cache upload compensation', () => { vi.clearAllMocks() resetDbChainMock() mockInsertMetadata.mockReset().mockResolvedValue({ id: 'file-1' }) + mockInsertFileMetadata.mockReset().mockResolvedValue({ id: 'file-1' }) + mockDeleteFileMetadata.mockReset().mockResolvedValue(undefined) await rm(testDirectory, { recursive: true, force: true }) await mkdir(testDirectory, { recursive: true }) }) @@ -118,6 +124,28 @@ describe('local cache upload compensation', () => { ).rejects.toMatchObject({ code: 'ENOENT' }) }) + it.each(['execution', 'copilot'] as const)( + 'removes owned new local %s uploads if metadata persistence fails', + async (context) => { + const key = `${context}/unique-id/file.txt` + mockInsertFileMetadata.mockRejectedValueOnce(ORIGINAL_ERROR) + await expect( + uploadFile({ + file: Buffer.from('hello'), + fileName: 'file.txt', + customKey: key, + preserveKey: true, + cleanupOnMetadataFailure: true, + context, + contentType: 'text/plain', + metadata: { userId: 'user-1' }, + }) + ).rejects.toBe(ORIGINAL_ERROR) + await expect(stat(join(testDirectory, key))).rejects.toMatchObject({ code: 'ENOENT' }) + expect(mockDeleteFileMetadata).toHaveBeenCalledExactlyOnceWith(key) + } + ) + it('does not replace or delete a preexisting object', async () => { await writeOtherAttempt() diff --git a/apps/sim/lib/uploads/core/storage-service.test.ts b/apps/sim/lib/uploads/core/storage-service.test.ts index fe307baf115..43af9e525ac 100644 --- a/apps/sim/lib/uploads/core/storage-service.test.ts +++ b/apps/sim/lib/uploads/core/storage-service.test.ts @@ -11,6 +11,7 @@ const { mockUploadToS3, mockDeleteFromS3, mockInsertFileMetadata, + mockDeleteFileMetadata, mockInsertImmutableFileMetadata, mockCleanupUnboundKnowledgeUpload, mockGetSignedUrl, @@ -26,6 +27,7 @@ const { mockUploadToS3: vi.fn(), mockDeleteFromS3: vi.fn(), mockInsertFileMetadata: vi.fn(), + mockDeleteFileMetadata: vi.fn(), mockInsertImmutableFileMetadata: vi.fn(), mockCleanupUnboundKnowledgeUpload: vi.fn(), mockGetSignedUrl: vi.fn(), @@ -63,6 +65,7 @@ vi.mock('@/lib/uploads/providers/s3/client', () => ({ vi.mock('@/lib/uploads/server/metadata', () => ({ insertFileMetadata: mockInsertFileMetadata, + deleteFileMetadata: mockDeleteFileMetadata, insertImmutableFileMetadata: mockInsertImmutableFileMetadata, })) @@ -87,6 +90,8 @@ describe('createMultipartUpload', () => { mockAbort.mockResolvedValue(undefined) mockUploadToS3.mockResolvedValue({ key: 'k', path: 'p', name: 'k', size: 0, type: 'text/csv' }) mockInsertFileMetadata.mockResolvedValue({ id: 'file-1' }) + mockDeleteFileMetadata.mockResolvedValue(undefined) + mockDeleteFromS3.mockResolvedValue(undefined) mockInsertImmutableFileMetadata.mockResolvedValue({ id: 'file-1' }) mockCleanupUnboundKnowledgeUpload.mockResolvedValue(undefined) mockGetSignedUrl.mockResolvedValue('https://s3.example/create-only') @@ -252,6 +257,101 @@ describe('createMultipartUpload', () => { expect(mockCleanupUnboundKnowledgeUpload).not.toHaveBeenCalled() }) + it.each(['execution', 'copilot'] as const)( + 'removes an owned new %s object if metadata persistence fails, even after cancellation', + async (context) => { + const key = `${context}/new-file-id/file.txt` + const failure = new Error('metadata unavailable') + const controller = new AbortController() + mockUploadToS3.mockResolvedValueOnce({ key }) + mockInsertFileMetadata.mockImplementationOnce(async () => { + controller.abort(new Error('cancelled')) + throw failure + }) + + await expect( + uploadFile({ + file: Buffer.from('hello'), + fileName: 'file.txt', + customKey: key, + preserveKey: true, + cleanupOnMetadataFailure: true, + contentType: 'text/plain', + context, + metadata: { userId: 'user-1', workspaceId: 'workspace-1' }, + signal: controller.signal, + }) + ).rejects.toBe(failure) + + expect(mockDeleteFromS3).toHaveBeenCalledExactlyOnceWith( + key, + { bucket: 'b', region: 'r' }, + undefined + ) + expect(mockDeleteFileMetadata).toHaveBeenCalledExactlyOnceWith(key) + } + ) + + it('preserves the metadata error when cleanup of an owned object also fails', async () => { + const failure = new Error('metadata unavailable') + mockInsertFileMetadata.mockRejectedValueOnce(failure) + mockDeleteFromS3.mockRejectedValueOnce(new Error('storage unavailable')) + + await expect( + uploadFile({ + file: Buffer.from('hello'), + fileName: 'file.txt', + customKey: 'execution/new-file-id/file.txt', + preserveKey: true, + cleanupOnMetadataFailure: true, + contentType: 'text/plain', + context: 'execution', + metadata: { workspaceId: 'workspace-1' }, + }) + ).rejects.toBe(failure) + expect(mockDeleteFromS3).toHaveBeenCalledOnce() + expect(mockDeleteFileMetadata).not.toHaveBeenCalled() + }) + + it.each(['workspace', 'execution', 'copilot'] as const)( + 'leaves existing %s replacement keys untouched without explicit new-key ownership', + async (context) => { + const failure = new Error('metadata unavailable') + mockInsertFileMetadata.mockRejectedValueOnce(failure) + await expect( + uploadFile({ + file: Buffer.from('hello'), + fileName: 'existing.txt', + customKey: `${context}/existing.txt`, + preserveKey: true, + contentType: 'text/plain', + context, + metadata: { userId: 'user-1', workspaceId: 'workspace-1' }, + }) + ).rejects.toBe(failure) + expect(mockDeleteFromS3).not.toHaveBeenCalled() + expect(mockDeleteFileMetadata).not.toHaveBeenCalled() + } + ) + + it.each([ + { context: 'workspace', customKey: 'workspace/file.txt', preserveKey: true }, + { context: 'execution', customKey: undefined, preserveKey: true }, + { context: 'copilot', customKey: 'copilot/file.txt', preserveKey: false }, + ] as const)('rejects cleanup without an explicitly owned ephemeral key: %j', async (scope) => { + await expect( + uploadFile({ + ...scope, + file: Buffer.from('hello'), + fileName: 'file.txt', + contentType: 'text/plain', + cleanupOnMetadataFailure: true, + metadata: { userId: 'user-1' }, + }) + ).rejects.toThrow('newly allocated execution or Copilot key') + expect(mockUploadToS3).not.toHaveBeenCalled() + }) + it('takes the single-shot PutObject path for a payload smaller than one part', async () => { const handle = await createMultipartUpload({ key: 'k', diff --git a/apps/sim/lib/uploads/core/storage-service.ts b/apps/sim/lib/uploads/core/storage-service.ts index dfd2edc83c1..ee67df9704a 100644 --- a/apps/sim/lib/uploads/core/storage-service.ts +++ b/apps/sim/lib/uploads/core/storage-service.ts @@ -98,7 +98,8 @@ async function insertFileMetadataHelper( fileName: string, contentType: string, fileSize: number, - uploadId?: string + uploadId?: string, + cleanupOnMetadataFailure = false ): Promise { const { insertFileMetadata, insertImmutableFileMetadata } = await import( '@/lib/uploads/server/metadata' @@ -131,6 +132,18 @@ async function insertFileMetadataHelper( error: cleanupError, }) } + } else if (cleanupOnMetadataFailure) { + try { + await deleteFile({ key, context }) + const { deleteFileMetadata } = await import('@/lib/uploads/server/metadata') + await deleteFileMetadata(key) + } catch (cleanupError) { + logger.error('Failed to clean up an unpublished tool output upload', { + key, + context, + error: getErrorMessage(cleanupError), + }) + } } throw error } @@ -149,6 +162,7 @@ export async function uploadFile(options: UploadFileOptions): Promise customKey, metadata, persistMetadata = true, + cleanupOnMetadataFailure = false, createOnlyUploadId, signal, } = options @@ -156,6 +170,12 @@ export async function uploadFile(options: UploadFileOptions): Promise if (createOnlyUploadId && (context !== 'knowledge-base' || !metadata)) { throw new Error('Reserved create-only uploads require knowledge-base ownership metadata') } + if ( + cleanupOnMetadataFailure && + ((context !== 'execution' && context !== 'copilot') || !preserveKey || !customKey) + ) { + throw new Error('Upload cleanup requires a newly allocated execution or Copilot key') + } logger.info(`Uploading file to ${context} storage: ${fileName}`) @@ -190,7 +210,8 @@ export async function uploadFile(options: UploadFileOptions): Promise fileName, contentType, file.length, - uploadId + uploadId, + cleanupOnMetadataFailure ) } @@ -219,7 +240,8 @@ export async function uploadFile(options: UploadFileOptions): Promise fileName, contentType, file.length, - uploadId + uploadId, + cleanupOnMetadataFailure ) } @@ -248,7 +270,8 @@ export async function uploadFile(options: UploadFileOptions): Promise fileName, contentType, file.length, - uploadId + uploadId, + cleanupOnMetadataFailure ) } @@ -295,7 +318,8 @@ export async function uploadFile(options: UploadFileOptions): Promise fileName, contentType, file.length, - uploadId + uploadId, + cleanupOnMetadataFailure ) } diff --git a/apps/sim/lib/uploads/shared/types.ts b/apps/sim/lib/uploads/shared/types.ts index f8d58b86e0f..9d846f1c0d2 100644 --- a/apps/sim/lib/uploads/shared/types.ts +++ b/apps/sim/lib/uploads/shared/types.ts @@ -127,6 +127,8 @@ export interface UploadFileOptions { * Disable when a caller finalizes metadata in its own database transaction. */ persistMetadata?: boolean + /** Only for newly allocated, unique execution or Copilot keys; never enable for replacements. */ + cleanupOnMetadataFailure?: boolean /** Internal create-only upload identity when metadata and cleanup were reserved before writing bytes. */ createOnlyUploadId?: string signal?: AbortSignal diff --git a/apps/sim/lib/uploads/utils/attachment-download-budget.test.ts b/apps/sim/lib/uploads/utils/attachment-download-budget.test.ts new file mode 100644 index 00000000000..c68fa225e6c --- /dev/null +++ b/apps/sim/lib/uploads/utils/attachment-download-budget.test.ts @@ -0,0 +1,61 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' +import { + AttachmentDownloadBudget, + readAttachmentJson, +} from '@/lib/uploads/utils/attachment-download-budget' + +describe('AttachmentDownloadBudget', () => { + it('defaults to the shared 100 MiB transfer bound', () => { + expect(new AttachmentDownloadBudget().remainingBytes).toBe(MAX_BUFFERED_TRANSFER_BYTES) + }) + + it('shares the remaining bytes across downloads and accepts the exact boundary', async () => { + const budget = new AttachmentDownloadBudget({ maxBytes: 6 }) + await budget.read(new Response('abc'), 'attachments') + await budget.read(new Response('def'), 'attachments') + expect(budget.remainingBytes).toBe(0) + await expect(budget.read(new Response('g'), 'attachments')).rejects.toBeInstanceOf( + PayloadSizeLimitError + ) + expect(budget.remainingBytes).toBe(0) + }) + + it('enforces actual bytes even with a false content-length', async () => { + const budget = new AttachmentDownloadBudget({ maxBytes: 3 }) + await expect( + budget.read(new Response('four', { headers: { 'content-length': '1' } }), 'attachments') + ).rejects.toBeInstanceOf(PayloadSizeLimitError) + }) + + it('accepts zero-byte files at the exact aggregate boundary', async () => { + const budget = new AttachmentDownloadBudget({ maxBytes: 0 }) + expect((await budget.read(new Response(''), 'attachments')).byteLength).toBe(0) + }) + + it('propagates cancellation while reading a body', async () => { + const controller = new AbortController() + const budget = new AttachmentDownloadBudget({ signal: controller.signal }) + const stream = new ReadableStream({ + pull() { + controller.abort(new DOMException('cancelled', 'AbortError')) + }, + }) + await expect(budget.read(new Response(stream), 'attachments')).rejects.toMatchObject({ + name: 'AbortError', + }) + }) + + it('keeps metadata responses bounded separately from file content', async () => { + await expect( + readAttachmentJson( + new Response('{}', { headers: { 'content-length': String(10 * 1024 * 1024 + 1) } }), + 'metadata' + ) + ).rejects.toBeInstanceOf(PayloadSizeLimitError) + }) +}) diff --git a/apps/sim/lib/uploads/utils/attachment-download-budget.ts b/apps/sim/lib/uploads/utils/attachment-download-budget.ts new file mode 100644 index 00000000000..15cdb5520bb --- /dev/null +++ b/apps/sim/lib/uploads/utils/attachment-download-budget.ts @@ -0,0 +1,70 @@ +import { + assertKnownSizeWithinLimit, + DEFAULT_MAX_ERROR_BODY_BYTES, + isPayloadSizeLimitError, + readResponseTextWithLimit, + readResponseToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' +import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' + +const MAX_ATTACHMENT_METADATA_BYTES = 10 * 1024 * 1024 + +/** A shared byte budget for sequential attachment downloads in one tool call. */ +export class AttachmentDownloadBudget { + private downloadedBytes = 0 + readonly signal?: AbortSignal + private readonly maxBytes: number + + constructor(options: { signal?: AbortSignal; maxBytes?: number } = {}) { + this.signal = options.signal + this.maxBytes = options.maxBytes ?? MAX_BUFFERED_TRANSFER_BYTES + } + + get remainingBytes(): number { + return this.maxBytes - this.downloadedBytes + } + + assertSize(size: number, label: string, maxFileBytes = this.maxBytes): void { + this.signal?.throwIfAborted() + assertKnownSizeWithinLimit(size, Math.min(this.remainingBytes, maxFileBytes), label) + } + + consume(buffer: Buffer, label: string): Buffer { + this.assertSize(buffer.byteLength, label) + this.downloadedBytes += buffer.byteLength + return buffer + } + + async read(response: Response, label: string, maxFileBytes = this.maxBytes): Promise { + this.signal?.throwIfAborted() + const buffer = await readResponseToBufferWithLimit(response, { + maxBytes: Math.min(this.remainingBytes, maxFileBytes), + label, + signal: this.signal, + }) + return this.consume(buffer, label) + } +} + +/** Provider metadata and error bodies remain bounded independently of file content. */ +export async function readAttachmentJson( + response: Response, + label: string, + signal?: AbortSignal, + maxBytes = MAX_ATTACHMENT_METADATA_BYTES +): Promise { + signal?.throwIfAborted() + const text = await readResponseTextWithLimit(response, { + maxBytes: response.ok ? maxBytes : DEFAULT_MAX_ERROR_BODY_BYTES, + label, + signal, + }) + signal?.throwIfAborted() + return JSON.parse(text) as T +} + +/** Partial attachment failures may be skipped, but cancellation and byte limits must stop the call. */ +export function rethrowAttachmentDownloadError(error: unknown, signal?: AbortSignal): void { + signal?.throwIfAborted() + if (isPayloadSizeLimitError(error)) throw error +} diff --git a/apps/sim/lib/uploads/utils/stored-file-metadata.ts b/apps/sim/lib/uploads/utils/stored-file-metadata.ts new file mode 100644 index 00000000000..d1cee88a972 --- /dev/null +++ b/apps/sim/lib/uploads/utils/stored-file-metadata.ts @@ -0,0 +1,33 @@ +import { sniffImageContentType } from '@/lib/uploads/utils/validation' + +const IMAGE_FILE_EXTENSIONS: Record = { + 'image/gif': 'gif', + 'image/jpeg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp', +} + +/** Derives stored image metadata from its bytes rather than trusting a provider's MIME type. */ +export function resolveStoredFileMetadata( + fileName: string, + declaredMimeType: string, + buffer: Buffer +): { fileName: string; mimeType: string } { + if (!declaredMimeType.startsWith('image/')) { + return { fileName, mimeType: declaredMimeType } + } + + const mimeType = sniffImageContentType(buffer) + if (!mimeType) { + return { + fileName: `${fileName.replace(/\.[^.]+$/, '')}.bin`, + mimeType: 'application/octet-stream', + } + } + + const extension = IMAGE_FILE_EXTENSIONS[mimeType] + return { + fileName: extension ? `${fileName.replace(/\.[^.]+$/, '')}.${extension}` : fileName, + mimeType, + } +} diff --git a/apps/sim/tools/agiloft/retrieve_attachment.test.ts b/apps/sim/tools/agiloft/retrieve_attachment.test.ts new file mode 100644 index 00000000000..611f53b8abc --- /dev/null +++ b/apps/sim/tools/agiloft/retrieve_attachment.test.ts @@ -0,0 +1,23 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { agiloftRetrieveAttachmentTool } from '@/tools/agiloft/retrieve_attachment' + +describe('Agiloft attachment file output', () => { + it('preserves the canonical stored file descriptor without requiring inline data', async () => { + const file = { + id: 'stored-file', + name: 'attachment.pdf', + size: 12 * 1024 * 1024, + type: 'application/pdf', + url: '/api/files/stored', + key: 'execution/attachment.pdf', + context: 'execution', + } + const result = await agiloftRetrieveAttachmentTool.transformResponse!( + Response.json({ success: true, output: { file } }) + ) + expect(result).toEqual({ success: true, output: { file } }) + expect(result.output.file).not.toHaveProperty('data') + expect(result.output.file).not.toHaveProperty('mimeType') + }) +}) diff --git a/apps/sim/tools/agiloft/retrieve_attachment.ts b/apps/sim/tools/agiloft/retrieve_attachment.ts index ad2b2ab1953..b7f02e0f87f 100644 --- a/apps/sim/tools/agiloft/retrieve_attachment.ts +++ b/apps/sim/tools/agiloft/retrieve_attachment.ts @@ -93,12 +93,7 @@ export const agiloftRetrieveAttachmentTool: InternalToolConfig< return { success: true, output: { - file: { - name: data.output.file.name, - mimeType: data.output.file.mimeType, - data: data.output.file.data, - size: data.output.file.size, - }, + file: data.output.file, }, } }, diff --git a/apps/sim/tools/agiloft/types.ts b/apps/sim/tools/agiloft/types.ts index f21eb5c265c..2ef86af87e6 100644 --- a/apps/sim/tools/agiloft/types.ts +++ b/apps/sim/tools/agiloft/types.ts @@ -1,4 +1,5 @@ -import type { ToolResponse } from '@/tools/types' +import type { UserFile } from '@/executor/types' +import type { ToolFileData, ToolResponse } from '@/tools/types' /** * Connection and credentials. `table` is optional here because EWLogin is @@ -149,12 +150,7 @@ export interface AgiloftRetrieveAttachmentParams extends AgiloftBaseParams { export interface AgiloftRetrieveAttachmentResponse extends ToolResponse { output: { - file: { - name: string - mimeType: string - data: string - size: number - } + file: UserFile | ToolFileData } } diff --git a/apps/sim/tools/binary-downloads.test.ts b/apps/sim/tools/binary-downloads.test.ts new file mode 100644 index 00000000000..d08f4a94893 --- /dev/null +++ b/apps/sim/tools/binary-downloads.test.ts @@ -0,0 +1,235 @@ +/** @vitest-environment node */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { boxDownloadFileTool, boxDownloadFileV2Tool } from '@/tools/box/download_file' +import { daytonaDownloadFileTool } from '@/tools/daytona/download_file' +import { dropboxDownloadTool, dropboxDownloadV2Tool } from '@/tools/dropbox/download' +import { getQrCodeTool, getQrCodeV2Tool } from '@/tools/dub/get_qr_code' +import { + dataverseDownloadFileTool, + dataverseDownloadFileV2Tool, +} from '@/tools/microsoft_dataverse/download_file' +import { personaPrintInquiryPdfTool } from '@/tools/persona/print_inquiry_pdf' +import { s3GetObjectTool } from '@/tools/s3/get_object' +import { + downloadAttachmentTool, + downloadAttachmentV2Tool, +} from '@/tools/servicenow/download_attachment' +import { storageDownloadTool } from '@/tools/supabase/storage_download' + +interface BinaryResult { + success: boolean + output: Record +} + +interface DownloadCase { + tool: { id: string; request: { responseType?: 'binary' }; outputs?: Record } + transform: (response: Response) => Promise + name: string + mimeType?: string + outputKeys?: string[] + checkMetadata?: (output: Record, size: number) => void +} + +const DOWNLOAD_CASES: DownloadCase[] = [ + { + tool: boxDownloadFileV2Tool, + transform: (response) => boxDownloadFileV2Tool.transformResponse!(response), + name: 'download.pdf', + outputKeys: ['file'], + }, + { + tool: dropboxDownloadV2Tool, + transform: (response) => + dropboxDownloadV2Tool.transformResponse!(response, { path: '/download.pdf' }), + name: 'download.pdf', + outputKeys: ['file', 'metadata', 'temporaryLink'], + checkMetadata: (output, size) => { + expect(output.metadata).toEqual({ id: 'file-1', name: 'download.pdf', size }) + expect(output.temporaryLink).toBeUndefined() + }, + }, + { + tool: s3GetObjectTool, + transform: (response) => + s3GetObjectTool.transformResponse!(response, { + accessKeyId: 'test-access-key', + secretAccessKey: 'test-secret-key', + bucketName: 'test-bucket', + region: 'us-east-1', + objectKey: 'folder/download.pdf', + }), + name: 'download.pdf', + checkMetadata: (output, size) => { + expect(output.metadata).toEqual({ + fileType: 'application/pdf', + size, + name: 'download.pdf', + lastModified: 'Fri, 11 Sep 2026 12:00:00 GMT', + }) + expect(output.url).toMatch( + /^https:\/\/test-bucket\.s3\.us-east-1\.amazonaws\.com\/folder\/download\.pdf\?/ + ) + }, + }, + { + tool: storageDownloadTool, + transform: (response) => + storageDownloadTool.transformResponse!(response, { + projectId: 'project-1', + apiKey: 'test-key', + bucket: 'documents', + path: 'folder/original.pdf', + fileName: 'renamed.pdf', + }), + name: 'renamed.pdf', + }, + { + tool: daytonaDownloadFileTool, + transform: (response) => + daytonaDownloadFileTool.transformResponse!(response, { + apiKey: 'test-key', + sandboxId: 'sandbox-1', + filePath: '/workspace/download.pdf', + }), + name: 'download.pdf', + checkMetadata: (output, size) => { + expect(output.name).toBe('download.pdf') + expect(output.mimeType).toBe('application/pdf') + expect(output.size).toBe(size) + }, + }, + { + tool: dataverseDownloadFileV2Tool, + transform: (response) => + dataverseDownloadFileV2Tool.transformResponse!(response, { + accessToken: 'test-token', + environmentUrl: 'https://test.crm.dynamics.com', + entitySetName: 'accounts', + recordId: 'record-1', + fileColumn: 'cr_document', + }), + name: 'download.pdf', + outputKeys: ['file', 'fileColumn'], + checkMetadata: (output) => { + expect(output.fileColumn).toBe('cr_document') + }, + }, + { + tool: personaPrintInquiryPdfTool, + transform: (response) => + personaPrintInquiryPdfTool.transformResponse!(response, { + apiKey: 'test-key', + inquiryId: 'inq_test', + }), + name: 'inq_test.pdf', + }, + { + tool: downloadAttachmentV2Tool, + transform: (response) => downloadAttachmentV2Tool.transformResponse!(response), + name: 'download.pdf', + outputKeys: ['file'], + }, + { + tool: getQrCodeV2Tool, + transform: (response) => getQrCodeV2Tool.transformResponse!(response), + name: 'qrcode.png', + mimeType: 'image/png', + outputKeys: ['file'], + }, +] + +function binaryResponse(buffer: Buffer, mimeType = 'application/pdf'): Response { + return new Response(buffer, { + headers: { + 'content-type': mimeType, + 'content-length': String(buffer.length), + 'content-disposition': 'attachment; filename="download.pdf"', + 'last-modified': 'Fri, 11 Sep 2026 12:00:00 GMT', + 'dropbox-api-result': JSON.stringify({ + id: 'file-1', + name: 'download.pdf', + size: buffer.length, + }), + 'x-ms-file-name': 'download.pdf', + 'x-ms-file-size': String(buffer.length), + }, + }) +} + +function expectBinaryFile(result: BinaryResult, buffer: Buffer, provider: DownloadCase): void { + expect(result.success).toBe(true) + const file = result.output.file as { + name: unknown + mimeType: unknown + size: unknown + data: unknown + } + expect(file.name).toBe(provider.name) + expect(file.mimeType).toBe(provider.mimeType ?? 'application/pdf') + expect(file.size).toBe(buffer.length) + expect(Buffer.isBuffer(file.data)).toBe(true) + if (!Buffer.isBuffer(file.data)) throw new Error('Expected raw file bytes') + expect(file.data.length).toBe(buffer.length) + expect(file.data.equals(buffer)).toBe(true) + expect(result.output).not.toHaveProperty('content') + expect(result.output).not.toHaveProperty('fileContent') + if (provider.outputKeys) { + expect(Object.keys(result.output).sort()).toEqual(provider.outputKeys) + } + provider.checkMetadata?.(result.output, buffer.length) +} + +beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()) +}) + +afterEach(() => { + expect(fetch).not.toHaveBeenCalled() + vi.unstubAllGlobals() +}) + +describe.each(DOWNLOAD_CASES)('$tool.id binary download', (provider) => { + it('opts into the bounded binary transfer budget', () => { + expect(provider.tool.request.responseType).toBe('binary') + expect(provider.tool.outputs).not.toHaveProperty('content') + expect(provider.tool.outputs).not.toHaveProperty('fileContent') + if (provider.outputKeys) { + expect(Object.keys(provider.tool.outputs!).sort()).toEqual(provider.outputKeys) + } + }) + + it('returns raw bytes and file metadata above the former 10 MiB cap', async () => { + const buffer = Buffer.alloc(11 * 1024 * 1024, 65) + const result = await provider.transform(binaryResponse(buffer, provider.mimeType)) + expectBinaryFile(result, buffer, provider) + }) + + it('returns small downloads without inline content', async () => { + const buffer = Buffer.from('small file contents') + const result = await provider.transform(binaryResponse(buffer, provider.mimeType)) + expectBinaryFile(result, buffer, provider) + }) +}) + +const LEGACY_DOWNLOAD_CASES = [ + { tool: boxDownloadFileTool, content: 'content' }, + { tool: dropboxDownloadTool, content: 'content' }, + { tool: getQrCodeTool, content: 'content' }, + { tool: dataverseDownloadFileTool, content: 'fileContent' }, + { tool: downloadAttachmentTool, content: 'content' }, +] as const + +describe.each(LEGACY_DOWNLOAD_CASES)( + '$tool.id legacy download compatibility', + ({ tool, content }) => { + it('retains the original response budget and inline base64 contract', async () => { + expect(tool.request).not.toHaveProperty('responseType') + const buffer = Buffer.from('saved workflow content') + const result = await tool.transformResponse(binaryResponse(buffer)) + expect(result.success).toBe(true) + expect(result.output).toHaveProperty(content, buffer.toString('base64')) + expect(result.output.file?.data).toBe(buffer.toString('base64')) + expect(tool.outputs).toHaveProperty(content) + }) + } +) diff --git a/apps/sim/tools/box/download_file.ts b/apps/sim/tools/box/download_file.ts index 24366105742..20ea1ffdf46 100644 --- a/apps/sim/tools/box/download_file.ts +++ b/apps/sim/tools/box/download_file.ts @@ -1,7 +1,50 @@ -import type { ToolConfig } from '@/tools/types' -import type { BoxDownloadFileParams, BoxDownloadFileResponse } from './types' +import { omit } from '@sim/utils/object' +import type { + BoxDownloadFileParams, + BoxDownloadFileResponse, + BoxDownloadFileV2Response, +} from '@/tools/box/types' +import type { ToolConfig, ToolFileData } from '@/tools/types' -export const boxDownloadFileTool: ToolConfig = { +async function transformDownloadResponse(response: Response) { + if (response.status === 202) { + const retryAfter = response.headers.get('retry-after') || 'a few' + throw new Error(`File is not yet ready for download. Retry after ${retryAfter} seconds.`) + } + + if (!response.ok) { + const errorText = await response.text() + throw new Error(errorText || `Failed to download file: ${response.status}`) + } + + const contentType = response.headers.get('content-type') || 'application/octet-stream' + const contentDisposition = response.headers.get('content-disposition') + let fileName = 'download' + + if (contentDisposition) { + const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) + if (match?.[1]) { + fileName = match[1].replace(/['"]/g, '') + } + } + + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + return { + success: true, + output: { + file: { + name: fileName, + mimeType: contentType, + data: buffer, + size: buffer.length, + }, + }, + } +} + +export const boxDownloadFileTool = { id: 'box_download_file', name: 'Box Download File', description: 'Download a file from Box', @@ -36,40 +79,15 @@ export const boxDownloadFileTool: ToolConfig { - if (response.status === 202) { - const retryAfter = response.headers.get('retry-after') || 'a few' - throw new Error(`File is not yet ready for download. Retry after ${retryAfter} seconds.`) - } - - if (!response.ok) { - const errorText = await response.text() - throw new Error(errorText || `Failed to download file: ${response.status}`) - } - - const contentType = response.headers.get('content-type') || 'application/octet-stream' - const contentDisposition = response.headers.get('content-disposition') - let fileName = 'download' - - if (contentDisposition) { - const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) - if (match?.[1]) { - fileName = match[1].replace(/['"]/g, '') - } - } - - const arrayBuffer = await response.arrayBuffer() - const buffer = Buffer.from(arrayBuffer) - + const result = await transformDownloadResponse(response) + const file = result.output.file + const content = file.data.toString('base64') return { - success: true, + ...result, output: { - file: { - name: fileName, - mimeType: contentType, - data: buffer.toString('base64'), - size: buffer.length, - }, - content: buffer.toString('base64'), + ...result.output, + file: { ...file, data: content }, + content, }, } }, @@ -84,4 +102,16 @@ export const boxDownloadFileTool: ToolConfig + +export const boxDownloadFileV2Tool: ToolConfig< + BoxDownloadFileParams, + BoxDownloadFileV2Response +> = { + ...boxDownloadFileTool, + id: 'box_download_file_v2', + version: '2.0.0', + request: { ...boxDownloadFileTool.request, responseType: 'binary' }, + transformResponse: transformDownloadResponse, + outputs: omit(boxDownloadFileTool.outputs, ['content']), } diff --git a/apps/sim/tools/box/index.ts b/apps/sim/tools/box/index.ts index 2e3b748fb42..a9bd6e3cbf5 100644 --- a/apps/sim/tools/box/index.ts +++ b/apps/sim/tools/box/index.ts @@ -2,7 +2,7 @@ export { boxCopyFileTool } from '@/tools/box/copy_file' export { boxCreateFolderTool } from '@/tools/box/create_folder' export { boxDeleteFileTool } from '@/tools/box/delete_file' export { boxDeleteFolderTool } from '@/tools/box/delete_folder' -export { boxDownloadFileTool } from '@/tools/box/download_file' +export { boxDownloadFileTool, boxDownloadFileV2Tool } from '@/tools/box/download_file' export { boxGetFileInfoTool } from '@/tools/box/get_file_info' export { boxListFolderItemsTool } from '@/tools/box/list_folder_items' export { boxSearchTool } from '@/tools/box/search' diff --git a/apps/sim/tools/box/types.ts b/apps/sim/tools/box/types.ts index 07368f79984..c5463b3089c 100644 --- a/apps/sim/tools/box/types.ts +++ b/apps/sim/tools/box/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { OutputProperty, ToolResponse } from '@/tools/types' export interface BoxUploadFileParams { @@ -95,6 +96,12 @@ export interface BoxDownloadFileResponse extends ToolResponse { } } +export interface BoxDownloadFileV2Response extends ToolResponse { + output: Omit & { + file: File + } +} + export interface BoxFileInfoResponse extends ToolResponse { output: { id: string diff --git a/apps/sim/tools/cursor/download_artifact.test.ts b/apps/sim/tools/cursor/download_artifact.test.ts new file mode 100644 index 00000000000..dc32b3f1953 --- /dev/null +++ b/apps/sim/tools/cursor/download_artifact.test.ts @@ -0,0 +1,33 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { downloadArtifactTool, downloadArtifactV2Tool } from '@/tools/cursor/download_artifact' + +describe('Cursor artifact output versions', () => { + it('preserves legacy inline metadata', async () => { + const file = { name: 'index.ts', mimeType: 'text/plain', data: 'YQ==', size: 1 } + const result = await downloadArtifactTool.transformResponse!( + Response.json({ success: true, output: { file } }) + ) + expect(result).toEqual({ + success: true, + output: { content: 'Downloaded artifact: index.ts', metadata: file }, + }) + }) + + it('preserves only the canonical stored file in v2', async () => { + const file = { + id: 'stored-file', + name: 'index.ts', + size: 12 * 1024 * 1024, + type: 'text/plain', + url: '/api/files/stored', + key: 'execution/index.ts', + context: 'execution', + } + const result = await downloadArtifactV2Tool.transformResponse!( + Response.json({ success: true, output: { file } }) + ) + expect(result).toEqual({ success: true, output: { file } }) + expect(Object.keys(downloadArtifactV2Tool.outputs!)).toEqual(['file']) + }) +}) diff --git a/apps/sim/tools/cursor/types.ts b/apps/sim/tools/cursor/types.ts index 2dfcf38fa20..31159e509bd 100644 --- a/apps/sim/tools/cursor/types.ts +++ b/apps/sim/tools/cursor/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' interface BaseCursorParams { @@ -218,12 +219,7 @@ export interface DownloadArtifactResponse extends ToolResponse { export interface DownloadArtifactV2Response extends ToolResponse { output: { - file: { - name: string - mimeType: string - data: string - size: number - } + file: UserFile } } diff --git a/apps/sim/tools/daytona/download_file.ts b/apps/sim/tools/daytona/download_file.ts index 201bd6fa5af..1b2c5402840 100644 --- a/apps/sim/tools/daytona/download_file.ts +++ b/apps/sim/tools/daytona/download_file.ts @@ -40,6 +40,7 @@ export const daytonaDownloadFileTool: ToolConfig< }, request: { + responseType: 'binary', url: (params) => daytonaToolboxUrl( params.sandboxId, @@ -75,7 +76,7 @@ export const daytonaDownloadFileTool: ToolConfig< file: { name: fileName, mimeType, - data: buffer.toString('base64'), + data: buffer, size: buffer.length, }, name: fileName, diff --git a/apps/sim/tools/dropbox/download.ts b/apps/sim/tools/dropbox/download.ts index 011e1a64a83..dbd57e8894c 100644 --- a/apps/sim/tools/dropbox/download.ts +++ b/apps/sim/tools/dropbox/download.ts @@ -1,8 +1,66 @@ +import { omit } from '@sim/utils/object' import { httpHeaderSafeJson } from '@/lib/core/utils/validation' -import type { DropboxDownloadParams, DropboxDownloadResponse } from '@/tools/dropbox/types' -import type { ToolConfig } from '@/tools/types' +import type { + DropboxDownloadParams, + DropboxDownloadResponse, + DropboxDownloadV2Response, +} from '@/tools/dropbox/types' +import type { ToolConfig, ToolFileData } from '@/tools/types' -export const dropboxDownloadTool: ToolConfig = { +async function transformDownloadResponse(response: Response, params?: DropboxDownloadParams) { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: errorText || 'Failed to download file', + output: {}, + } + } + + const apiResultHeader = + response.headers.get('dropbox-api-result') || response.headers.get('Dropbox-API-Result') + const metadata = apiResultHeader ? JSON.parse(apiResultHeader) : undefined + const contentType = response.headers.get('content-type') || 'application/octet-stream' + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + const resolvedName = metadata?.name || params?.path?.split('/').pop() || 'download' + + let temporaryLink: string | undefined + if (params?.accessToken) { + try { + const linkResponse = await fetch('https://api.dropboxapi.com/2/files/get_temporary_link', { + method: 'POST', + headers: { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ path: params.path.trim() }), + }) + if (linkResponse.ok) { + const linkData = await linkResponse.json() + temporaryLink = linkData.link + } + } catch { + temporaryLink = undefined + } + } + + return { + success: true, + output: { + file: { + name: resolvedName, + mimeType: contentType, + data: buffer, + size: buffer.length, + }, + metadata, + temporaryLink, + }, + } +} + +export const dropboxDownloadTool = { id: 'dropbox_download', name: 'Dropbox Download File', description: 'Download a file from Dropbox with metadata and content', @@ -38,55 +96,16 @@ export const dropboxDownloadTool: ToolConfig { - if (!response.ok) { - const errorText = await response.text() - return { - success: false, - error: errorText || 'Failed to download file', - output: {}, - } - } - - const apiResultHeader = - response.headers.get('dropbox-api-result') || response.headers.get('Dropbox-API-Result') - const metadata = apiResultHeader ? JSON.parse(apiResultHeader) : undefined - const contentType = response.headers.get('content-type') || 'application/octet-stream' - const arrayBuffer = await response.arrayBuffer() - const buffer = Buffer.from(arrayBuffer) - const resolvedName = metadata?.name || params?.path?.split('/').pop() || 'download' - - let temporaryLink: string | undefined - if (params?.accessToken) { - try { - const linkResponse = await fetch('https://api.dropboxapi.com/2/files/get_temporary_link', { - method: 'POST', - headers: { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ path: params.path.trim() }), - }) - if (linkResponse.ok) { - const linkData = await linkResponse.json() - temporaryLink = linkData.link - } - } catch { - temporaryLink = undefined - } - } - + const result = await transformDownloadResponse(response, params) + if (!result.success || !result.output.file) return result + const file = result.output.file + const content = file.data.toString('base64') return { - success: true, + ...result, output: { - file: { - name: resolvedName, - mimeType: contentType, - data: buffer.toString('base64'), - size: buffer.length, - }, - content: buffer.toString('base64'), - metadata, - temporaryLink, + ...result.output, + file: { ...file, data: content }, + content, }, } }, @@ -109,4 +128,17 @@ export const dropboxDownloadTool: ToolConfig + +export const dropboxDownloadV2Tool: ToolConfig< + DropboxDownloadParams, + DropboxDownloadV2Response +> = { + ...dropboxDownloadTool, + id: 'dropbox_download_v2', + description: 'Download a file from Dropbox with metadata', + version: '2.0.0', + request: { ...dropboxDownloadTool.request, responseType: 'binary' }, + transformResponse: transformDownloadResponse, + outputs: omit(dropboxDownloadTool.outputs, ['content']), } diff --git a/apps/sim/tools/dropbox/index.ts b/apps/sim/tools/dropbox/index.ts index c113cc121af..6e9dc450113 100644 --- a/apps/sim/tools/dropbox/index.ts +++ b/apps/sim/tools/dropbox/index.ts @@ -2,7 +2,7 @@ import { dropboxCopyTool } from '@/tools/dropbox/copy' import { dropboxCreateFolderTool } from '@/tools/dropbox/create_folder' import { dropboxCreateSharedLinkTool } from '@/tools/dropbox/create_shared_link' import { dropboxDeleteTool } from '@/tools/dropbox/delete' -import { dropboxDownloadTool } from '@/tools/dropbox/download' +import { dropboxDownloadTool, dropboxDownloadV2Tool } from '@/tools/dropbox/download' import { dropboxGetMetadataTool } from '@/tools/dropbox/get_metadata' import { dropboxListFolderTool } from '@/tools/dropbox/list_folder' import { dropboxListRevisionsTool } from '@/tools/dropbox/list_revisions' @@ -18,6 +18,7 @@ export { dropboxCreateSharedLinkTool, dropboxDeleteTool, dropboxDownloadTool, + dropboxDownloadV2Tool, dropboxGetMetadataTool, dropboxListFolderTool, dropboxListRevisionsTool, diff --git a/apps/sim/tools/dropbox/types.ts b/apps/sim/tools/dropbox/types.ts index bfb5cf1c8ae..05c9594b9a4 100644 --- a/apps/sim/tools/dropbox/types.ts +++ b/apps/sim/tools/dropbox/types.ts @@ -1,4 +1,5 @@ import type { UserFileLike } from '@/lib/core/utils/user-file' +import type { UserFile } from '@/executor/types' import type { ToolFileData, ToolResponse } from '@/tools/types' interface DropboxFileMetadata { @@ -93,6 +94,12 @@ export interface DropboxDownloadResponse extends ToolResponse { } } +export interface DropboxDownloadV2Response extends ToolResponse { + output: Omit & { + file?: File + } +} + export interface DropboxListFolderParams extends DropboxBaseParams { path: string recursive?: boolean diff --git a/apps/sim/tools/dub/get_qr_code.ts b/apps/sim/tools/dub/get_qr_code.ts index 443ca643e38..24bf57d60a1 100644 --- a/apps/sim/tools/dub/get_qr_code.ts +++ b/apps/sim/tools/dub/get_qr_code.ts @@ -1,7 +1,42 @@ -import type { DubGetQrCodeParams, DubGetQrCodeResponse } from '@/tools/dub/types' -import type { ToolConfig } from '@/tools/types' +import { omit } from '@sim/utils/object' +import type { + DubGetQrCodeParams, + DubGetQrCodeResponse, + DubGetQrCodeV2Response, +} from '@/tools/dub/types' +import type { ToolConfig, ToolFileData } from '@/tools/types' -export const getQrCodeTool: ToolConfig = { +async function transformDownloadResponse(response: Response) { + if (!response.ok) { + const errorText = await response.text() + let message = errorText || `Failed to generate QR code: ${response.status}` + try { + const parsed = JSON.parse(errorText) + message = parsed.error?.message || parsed.error || message + } catch { + /** Non-JSON error body; use the raw text. */ + } + throw new Error(message) + } + + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + const mimeType = response.headers.get('content-type') || 'image/png' + + return { + success: true, + output: { + file: { + name: 'qrcode.png', + mimeType, + data: buffer, + size: buffer.length, + }, + }, + } +} + +export const getQrCodeTool = { id: 'dub_get_qr_code', name: 'Dub Get QR Code', description: @@ -85,33 +120,16 @@ export const getQrCodeTool: ToolConfig }), }, - transformResponse: async (response: Response) => { - if (!response.ok) { - const errorText = await response.text() - let message = errorText || `Failed to generate QR code: ${response.status}` - try { - const parsed = JSON.parse(errorText) - message = parsed.error?.message || parsed.error || message - } catch { - // Non-JSON error body; use the raw text - } - throw new Error(message) - } - - const arrayBuffer = await response.arrayBuffer() - const buffer = Buffer.from(arrayBuffer) - const mimeType = response.headers.get('content-type') || 'image/png' - + transformResponse: async (response) => { + const result = await transformDownloadResponse(response) + const file = result.output.file + const content = file.data.toString('base64') return { - success: true, + ...result, output: { - file: { - name: 'qrcode.png', - mimeType, - data: buffer.toString('base64'), - size: buffer.length, - }, - content: buffer.toString('base64'), + ...result.output, + file: { ...file, data: content }, + content, }, } }, @@ -126,4 +144,16 @@ export const getQrCodeTool: ToolConfig description: 'Base64-encoded PNG image data', }, }, +} satisfies ToolConfig + +export const getQrCodeV2Tool: ToolConfig< + DubGetQrCodeParams, + DubGetQrCodeV2Response +> = { + ...getQrCodeTool, + id: 'dub_get_qr_code_v2', + version: '2.0.0', + request: { ...getQrCodeTool.request, responseType: 'binary' }, + transformResponse: transformDownloadResponse, + outputs: omit(getQrCodeTool.outputs, ['content']), } diff --git a/apps/sim/tools/dub/index.ts b/apps/sim/tools/dub/index.ts index 09030412b92..7b7b957873f 100644 --- a/apps/sim/tools/dub/index.ts +++ b/apps/sim/tools/dub/index.ts @@ -8,7 +8,7 @@ import { getAnalyticsTool } from '@/tools/dub/get_analytics' import { getEventsTool } from '@/tools/dub/get_events' import { getLinkTool } from '@/tools/dub/get_link' import { getLinksCountTool } from '@/tools/dub/get_links_count' -import { getQrCodeTool } from '@/tools/dub/get_qr_code' +import { getQrCodeTool, getQrCodeV2Tool } from '@/tools/dub/get_qr_code' import { listDomainsTool } from '@/tools/dub/list_domains' import { listFoldersTool } from '@/tools/dub/list_folders' import { listLinksTool } from '@/tools/dub/list_links' @@ -29,6 +29,7 @@ export const dubBulkCreateLinksTool = bulkCreateLinksTool export const dubBulkUpdateLinksTool = bulkUpdateLinksTool export const dubBulkDeleteLinksTool = bulkDeleteLinksTool export const dubGetQrCodeTool = getQrCodeTool +export const dubGetQrCodeV2Tool = getQrCodeV2Tool export const dubListDomainsTool = listDomainsTool export const dubListTagsTool = listTagsTool export const dubCreateTagTool = createTagTool diff --git a/apps/sim/tools/dub/types.ts b/apps/sim/tools/dub/types.ts index 6270a26f3f8..9fea3443f39 100644 --- a/apps/sim/tools/dub/types.ts +++ b/apps/sim/tools/dub/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' interface DubBaseParams { @@ -303,6 +304,12 @@ export interface DubGetQrCodeResponse extends ToolResponse { } } +export interface DubGetQrCodeV2Response extends ToolResponse { + output: Omit & { + file: File + } +} + export interface DubListDomainsResponse extends ToolResponse { output: { domains: Record[] diff --git a/apps/sim/tools/file-message-operation-security.test.ts b/apps/sim/tools/file-message-operation-security.test.ts index 7015ce8ce55..5dfd134da78 100644 --- a/apps/sim/tools/file-message-operation-security.test.ts +++ b/apps/sim/tools/file-message-operation-security.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { assert, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ assertToolFileAccess: vi.fn(), @@ -68,6 +68,7 @@ import { executeDataverseUploadFile } from '@/lib/internal/microsoft-dataverse/o import { executePipedriveGetFiles } from '@/lib/internal/pipedrive/operations' import type { ServiceNowOperationError } from '@/lib/internal/servicenow/errors' import { executeServiceNowUploadAttachment } from '@/lib/internal/servicenow/operations' +import { isInternalToolFileResult } from '@/lib/internal/tool-operations/file-result' import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types' const FILE = { @@ -248,6 +249,25 @@ describe('file and message operation security', () => { MAX_BUFFERED_TRANSFER_BYTES, MAX_BUFFERED_TRANSFER_BYTES - 3, ]) - expect(result.output.downloadedFiles).toHaveLength(2) + assert(isInternalToolFileResult(result)) + expect(result.files).toEqual([ + { name: 'one.txt', mimeType: 'text/plain', buffer: Buffer.from('abc') }, + { name: 'two.txt', mimeType: 'text/plain', buffer: Buffer.from('defg') }, + ]) + const storedFiles = [ + { ...FILE, name: 'one.txt', mimeType: 'text/plain' }, + { + ...FILE, + id: 'file-2', + key: 'execution/file-2', + name: 'two.txt', + size: 4, + mimeType: 'text/plain', + }, + ] + expect(result.present(storedFiles)).toMatchObject({ + success: true, + output: { downloadedFiles: storedFiles, has_more: true, next_start: 2 }, + }) }) }) diff --git a/apps/sim/tools/generated/tool-ids.ts b/apps/sim/tools/generated/tool-ids.ts index ca9a23f2d24..67aa132eb1a 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","affinity_batch_update_entity_fields","affinity_batch_update_list_entry_fields","affinity_create_list","affinity_create_list_field_dropdown_option","affinity_create_merge","affinity_create_note","affinity_create_reminder","affinity_delete_list_field_dropdown_option","affinity_delete_note","affinity_get_company","affinity_get_current_user","affinity_get_entity_field_value","affinity_get_list","affinity_get_list_entry","affinity_get_list_entry_field","affinity_get_list_field_dropdown_option","affinity_get_merge","affinity_get_merge_task","affinity_get_note","affinity_get_opportunity","affinity_get_person","affinity_get_saved_view","affinity_get_transcript","affinity_get_user","affinity_list_calls","affinity_list_chat_messages","affinity_list_companies","affinity_list_coworker_connections","affinity_list_emails","affinity_list_entity_field_values","affinity_list_entity_list_entries","affinity_list_entity_lists","affinity_list_entity_notes","affinity_list_entity_relationships","affinity_list_field_dropdown_options","affinity_list_field_metadata","affinity_list_field_value_changes","affinity_list_investor_executive_connections","affinity_list_list_entries","affinity_list_list_entry_field_value_changes","affinity_list_list_entry_fields","affinity_list_list_field_dropdown_options","affinity_list_list_fields","affinity_list_lists","affinity_list_meetings","affinity_list_merge_tasks","affinity_list_merges","affinity_list_note_attached_companies","affinity_list_note_attached_opportunities","affinity_list_note_attached_persons","affinity_list_note_replies","affinity_list_notes","affinity_list_opportunities","affinity_list_persons","affinity_list_reminders","affinity_list_saved_view_entries","affinity_list_saved_views","affinity_list_transcript_fragments","affinity_list_transcripts","affinity_list_users","affinity_search_companies","affinity_search_files","affinity_search_list_entries","affinity_search_notes","affinity_search_persons","affinity_semantic_search","affinity_update_entity_field_value","affinity_update_list_entry_field","affinity_update_list_field_dropdown_option","affinity_update_note","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_get_opening","ashby_list_application_feedback","ashby_list_application_history","ashby_list_applications","ashby_list_archive_reasons","ashby_list_candidate_tags","ashby_list_candidates","ashby_list_custom_fields","ashby_list_departments","ashby_list_interview_plans","ashby_list_interview_stages","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_search_jobs","ashby_search_openings","ashby_search_users","ashby_set_custom_field_value","ashby_set_custom_field_values","ashby_transfer_application","ashby_update_candidate","ashby_upload_candidate_file","ashby_upload_resume","athena_batch_get_named_query","athena_batch_get_prepared_statement","athena_batch_get_query_execution","athena_create_named_query","athena_create_prepared_statement","athena_delete_named_query","athena_delete_prepared_statement","athena_get_data_catalog","athena_get_database","athena_get_named_query","athena_get_prepared_statement","athena_get_query_execution","athena_get_query_results","athena_get_query_runtime_statistics","athena_get_table_metadata","athena_get_work_group","athena_list_data_catalogs","athena_list_databases","athena_list_named_queries","athena_list_prepared_statements","athena_list_query_executions","athena_list_table_metadata","athena_list_work_groups","athena_start_query","athena_stop_query","athena_update_named_query","athena_update_prepared_statement","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","circleback_add_tag_to_meetings","circleback_create_tag","circleback_delete_action_item","circleback_delete_meeting","circleback_delete_tag","circleback_get_company","circleback_get_meeting","circleback_get_person","circleback_get_transcript","circleback_list_action_items","circleback_list_calendar_events","circleback_list_companies","circleback_list_meetings","circleback_list_people","circleback_list_tags","circleback_remove_tag_from_meetings","circleback_search_meetings","circleback_update_action_item","circleback_update_meeting","circleback_update_tag","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","cloudtrail_cancel_query","cloudtrail_describe_query","cloudtrail_describe_trails","cloudtrail_get_event_data_store","cloudtrail_get_event_selectors","cloudtrail_get_insight_selectors","cloudtrail_get_query_results","cloudtrail_get_trail","cloudtrail_get_trail_status","cloudtrail_list_event_data_stores","cloudtrail_list_tags","cloudtrail_list_trails","cloudtrail_lookup_events","cloudtrail_start_query","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_ollama","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_create_folder","file_decompress","file_delete_folder","file_edit","file_fetch","file_get","file_get_content","file_list","file_manage_sharing","file_move","file_parser","file_parser_v2","file_parser_v3","file_read","file_restore_folder","file_search","file_update_folder","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","harmonic_batch_get_people","harmonic_clear_people_saved_search_net_new_results","harmonic_enrich_person","harmonic_get_company_employees","harmonic_get_email_enrichment_job","harmonic_get_email_enrichment_usage","harmonic_get_enrichment_status","harmonic_get_people_saved_search_net_new_results","harmonic_get_people_saved_search_results","harmonic_get_person","harmonic_list_people_saved_searches","harmonic_search_people_scout","harmonic_submit_email_enrichment_job","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_policy","iam_get_role","iam_get_user","iam_list_access_keys","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","iam_update_access_key","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_describe_group","identity_center_describe_user","identity_center_get_group","identity_center_get_user","identity_center_list_account_assignments","identity_center_list_accounts","identity_center_list_assignments_for_account","identity_center_list_group_memberships","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","lambda_add_permission","lambda_create_alias","lambda_create_event_source_mapping","lambda_create_function","lambda_create_function_url_config","lambda_delete_alias","lambda_delete_event_source_mapping","lambda_delete_function","lambda_delete_function_concurrency","lambda_delete_function_event_invoke_config","lambda_delete_function_url_config","lambda_delete_provisioned_concurrency_config","lambda_get_account_settings","lambda_get_alias","lambda_get_event_source_mapping","lambda_get_function","lambda_get_function_concurrency","lambda_get_function_configuration","lambda_get_function_event_invoke_config","lambda_get_function_recursion_config","lambda_get_function_url_config","lambda_get_layer_version","lambda_get_policy","lambda_get_provisioned_concurrency_config","lambda_get_runtime_management_config","lambda_invoke","lambda_list_aliases","lambda_list_event_source_mappings","lambda_list_function_event_invoke_configs","lambda_list_function_url_configs","lambda_list_functions","lambda_list_layer_versions","lambda_list_layers","lambda_list_provisioned_concurrency_configs","lambda_list_tags","lambda_list_versions_by_function","lambda_publish_version","lambda_put_function_concurrency","lambda_put_function_event_invoke_config","lambda_put_function_recursion_config","lambda_put_provisioned_concurrency_config","lambda_put_runtime_management_config","lambda_remove_permission","lambda_tag_resource","lambda_untag_resource","lambda_update_alias","lambda_update_event_source_mapping","lambda_update_function_code","lambda_update_function_configuration","lambda_update_function_url_config","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","manageengine_sdp_add_change_note","manageengine_sdp_add_problem_note","manageengine_sdp_add_request_note","manageengine_sdp_create_asset","manageengine_sdp_create_change","manageengine_sdp_create_problem","manageengine_sdp_create_request","manageengine_sdp_create_solution","manageengine_sdp_delete_asset","manageengine_sdp_delete_change","manageengine_sdp_delete_problem","manageengine_sdp_delete_request","manageengine_sdp_delete_solution","manageengine_sdp_get_asset","manageengine_sdp_get_change","manageengine_sdp_get_problem","manageengine_sdp_get_request","manageengine_sdp_get_solution","manageengine_sdp_list_assets","manageengine_sdp_list_change_notes","manageengine_sdp_list_changes","manageengine_sdp_list_problem_notes","manageengine_sdp_list_problems","manageengine_sdp_list_request_notes","manageengine_sdp_list_requests","manageengine_sdp_list_solutions","manageengine_sdp_update_asset","manageengine_sdp_update_change","manageengine_sdp_update_problem","manageengine_sdp_update_request","manageengine_sdp_update_solution","mcp_list_operations","mcp_run_operation","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_dynamics_365_close_case","microsoft_dynamics_365_close_opportunity","microsoft_dynamics_365_create_record","microsoft_dynamics_365_get_record","microsoft_dynamics_365_list_records","microsoft_dynamics_365_qualify_lead","microsoft_dynamics_365_search_records","microsoft_dynamics_365_update_record","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","microsoft_word_append","microsoft_word_create","microsoft_word_create_from_template","microsoft_word_export_pdf","microsoft_word_list","microsoft_word_read","microsoft_word_replace_text","microsoft_word_update","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","modal_call_function","modal_chat_completion","modal_list_models","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","quickbooks_add_attachment","quickbooks_create_bill","quickbooks_create_bill_payment","quickbooks_create_credit_memo","quickbooks_create_customer","quickbooks_create_customer_payment","quickbooks_create_deposit","quickbooks_create_employee","quickbooks_create_estimate","quickbooks_create_invoice","quickbooks_create_item","quickbooks_create_journal_entry","quickbooks_create_purchase","quickbooks_create_purchase_order","quickbooks_create_refund_receipt","quickbooks_create_sales_receipt","quickbooks_create_vendor","quickbooks_create_vendor_credit","quickbooks_download_attachment","quickbooks_download_transaction_pdf","quickbooks_email_transaction","quickbooks_get_company_info","quickbooks_read_accounting_transactions","quickbooks_read_attachments","quickbooks_read_master_data","quickbooks_read_purchasing_transactions","quickbooks_read_sales_transactions","quickbooks_run_financial_report","quickbooks_update_bill","quickbooks_update_bill_payment","quickbooks_update_credit_memo","quickbooks_update_customer","quickbooks_update_customer_payment","quickbooks_update_deposit","quickbooks_update_employee","quickbooks_update_estimate","quickbooks_update_invoice","quickbooks_update_item","quickbooks_update_journal_entry","quickbooks_update_purchase","quickbooks_update_purchase_order","quickbooks_update_refund_receipt","quickbooks_update_sales_receipt","quickbooks_update_vendor","quickbooks_update_vendor_credit","quickbooks_void_bill_payment","quickbooks_void_customer_payment","quickbooks_void_invoice","quickbooks_void_sales_receipt","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","sailpoint_approve_access_request","sailpoint_cancel_access_request","sailpoint_decide_certification_review_items","sailpoint_get_access_profile","sailpoint_get_access_profile_entitlements","sailpoint_get_access_request_config","sailpoint_get_access_request_status","sailpoint_get_account","sailpoint_get_account_activity","sailpoint_get_account_entitlements","sailpoint_get_account_selections","sailpoint_get_campaign","sailpoint_get_certification","sailpoint_get_entitlement","sailpoint_get_entitlement_request_config","sailpoint_get_identity","sailpoint_get_role","sailpoint_get_role_entitlements","sailpoint_get_source","sailpoint_get_task_status","sailpoint_list_access_profiles","sailpoint_list_account_activities","sailpoint_list_accounts","sailpoint_list_campaigns","sailpoint_list_certification_review_items","sailpoint_list_certifications","sailpoint_list_entitlements","sailpoint_list_identities","sailpoint_list_identity_entitlements","sailpoint_list_pending_access_request_approvals","sailpoint_list_roles","sailpoint_list_sources","sailpoint_load_accounts","sailpoint_load_entitlements","sailpoint_reject_access_request","sailpoint_request_access","sailpoint_search","sailpoint_search_aggregate","sailpoint_search_count","sailpoint_sign_off_certification","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","semrush_backlinks","semrush_backlinks_anchors","semrush_backlinks_competitors","semrush_backlinks_geo_distribution","semrush_backlinks_indexed_pages","semrush_backlinks_overview","semrush_backlinks_tld_distribution","semrush_batch_keyword_overview","semrush_broad_match_keywords","semrush_domain_ad_copies","semrush_domain_ad_history","semrush_domain_organic_competitors","semrush_domain_organic_keywords","semrush_domain_overview","semrush_domain_overview_all","semrush_domain_overview_history","semrush_domain_paid_competitors","semrush_domain_paid_keywords","semrush_domain_pla_copies","semrush_domain_pla_keywords","semrush_domain_vs_domain","semrush_keyword_ad_history","semrush_keyword_difficulty","semrush_keyword_overview","semrush_keyword_overview_all","semrush_keyword_questions","semrush_organic_results","semrush_paid_results","semrush_referring_domains","semrush_referring_ips","semrush_related_keywords","semrush_subdomain_ad_copies","semrush_subdomain_organic_keywords","semrush_subdomain_overview","semrush_subdomain_overview_all","semrush_subdomain_overview_history","semrush_subdomain_paid_keywords","semrush_top_domains","semrush_url_organic_keywords","semrush_url_overview","semrush_url_overview_all","semrush_url_overview_history","semrush_url_paid_keywords","semrush_winners_and_losers","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_agent_session_v2","slack_rename_conversation","slack_schedule_message","slack_set_agent_session_status_v2","slack_set_conversation_purpose","slack_set_conversation_topic","slack_set_status","slack_set_suggested_prompts","slack_set_suggested_prompts_v2","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_cancel_message_move_task","sqs_change_message_visibility","sqs_change_message_visibility_batch","sqs_create_queue","sqs_delete_message","sqs_delete_message_batch","sqs_delete_queue","sqs_get_queue_attributes","sqs_get_queue_url","sqs_list_dead_letter_source_queues","sqs_list_message_move_tasks","sqs_list_queue_tags","sqs_list_queues","sqs_purge_queue","sqs_receive_message","sqs_send","sqs_send_message_batch","sqs_set_queue_attributes","sqs_start_message_move_task","sqs_tag_queue","sqs_untag_queue","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","ssm_cancel_command","ssm_delete_parameter","ssm_describe_automation_executions","ssm_describe_instance_information","ssm_describe_instance_patch_states","ssm_describe_instance_patches","ssm_describe_parameters","ssm_get_automation_execution","ssm_get_command_invocation","ssm_get_document","ssm_get_parameter","ssm_get_parameters","ssm_get_parameters_by_path","ssm_list_command_invocations","ssm_list_commands","ssm_list_compliance_items","ssm_list_compliance_summaries","ssm_list_documents","ssm_put_parameter","ssm_send_command","ssm_start_automation_execution","ssm_stop_automation_execution","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","tinyfish_cancel_run","tinyfish_fetch","tinyfish_get_run","tinyfish_list_profiles","tinyfish_list_runs","tinyfish_list_vault_items","tinyfish_run","tinyfish_run_async","tinyfish_search","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","affinity_batch_update_entity_fields","affinity_batch_update_list_entry_fields","affinity_create_list","affinity_create_list_field_dropdown_option","affinity_create_merge","affinity_create_note","affinity_create_reminder","affinity_delete_list_field_dropdown_option","affinity_delete_note","affinity_get_company","affinity_get_current_user","affinity_get_entity_field_value","affinity_get_list","affinity_get_list_entry","affinity_get_list_entry_field","affinity_get_list_field_dropdown_option","affinity_get_merge","affinity_get_merge_task","affinity_get_note","affinity_get_opportunity","affinity_get_person","affinity_get_saved_view","affinity_get_transcript","affinity_get_user","affinity_list_calls","affinity_list_chat_messages","affinity_list_companies","affinity_list_coworker_connections","affinity_list_emails","affinity_list_entity_field_values","affinity_list_entity_list_entries","affinity_list_entity_lists","affinity_list_entity_notes","affinity_list_entity_relationships","affinity_list_field_dropdown_options","affinity_list_field_metadata","affinity_list_field_value_changes","affinity_list_investor_executive_connections","affinity_list_list_entries","affinity_list_list_entry_field_value_changes","affinity_list_list_entry_fields","affinity_list_list_field_dropdown_options","affinity_list_list_fields","affinity_list_lists","affinity_list_meetings","affinity_list_merge_tasks","affinity_list_merges","affinity_list_note_attached_companies","affinity_list_note_attached_opportunities","affinity_list_note_attached_persons","affinity_list_note_replies","affinity_list_notes","affinity_list_opportunities","affinity_list_persons","affinity_list_reminders","affinity_list_saved_view_entries","affinity_list_saved_views","affinity_list_transcript_fragments","affinity_list_transcripts","affinity_list_users","affinity_search_companies","affinity_search_files","affinity_search_list_entries","affinity_search_notes","affinity_search_persons","affinity_semantic_search","affinity_update_entity_field_value","affinity_update_list_entry_field","affinity_update_list_field_dropdown_option","affinity_update_note","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_get_opening","ashby_list_application_feedback","ashby_list_application_history","ashby_list_applications","ashby_list_archive_reasons","ashby_list_candidate_tags","ashby_list_candidates","ashby_list_custom_fields","ashby_list_departments","ashby_list_interview_plans","ashby_list_interview_stages","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_search_jobs","ashby_search_openings","ashby_search_users","ashby_set_custom_field_value","ashby_set_custom_field_values","ashby_transfer_application","ashby_update_candidate","ashby_upload_candidate_file","ashby_upload_resume","athena_batch_get_named_query","athena_batch_get_prepared_statement","athena_batch_get_query_execution","athena_create_named_query","athena_create_prepared_statement","athena_delete_named_query","athena_delete_prepared_statement","athena_get_data_catalog","athena_get_database","athena_get_named_query","athena_get_prepared_statement","athena_get_query_execution","athena_get_query_results","athena_get_query_runtime_statistics","athena_get_table_metadata","athena_get_work_group","athena_list_data_catalogs","athena_list_databases","athena_list_named_queries","athena_list_prepared_statements","athena_list_query_executions","athena_list_table_metadata","athena_list_work_groups","athena_start_query","athena_stop_query","athena_update_named_query","athena_update_prepared_statement","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_download_file_v2","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","circleback_add_tag_to_meetings","circleback_create_tag","circleback_delete_action_item","circleback_delete_meeting","circleback_delete_tag","circleback_get_company","circleback_get_meeting","circleback_get_person","circleback_get_transcript","circleback_list_action_items","circleback_list_calendar_events","circleback_list_companies","circleback_list_meetings","circleback_list_people","circleback_list_tags","circleback_remove_tag_from_meetings","circleback_search_meetings","circleback_update_action_item","circleback_update_meeting","circleback_update_tag","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","cloudtrail_cancel_query","cloudtrail_describe_query","cloudtrail_describe_trails","cloudtrail_get_event_data_store","cloudtrail_get_event_selectors","cloudtrail_get_insight_selectors","cloudtrail_get_query_results","cloudtrail_get_trail","cloudtrail_get_trail_status","cloudtrail_list_event_data_stores","cloudtrail_list_tags","cloudtrail_list_trails","cloudtrail_lookup_events","cloudtrail_start_query","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_download_v2","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_get_qr_code_v2","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_ollama","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_create_folder","file_decompress","file_delete_folder","file_edit","file_fetch","file_get","file_get_content","file_list","file_manage_sharing","file_move","file_parser","file_parser_v2","file_parser_v3","file_read","file_restore_folder","file_search","file_update_folder","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","harmonic_batch_get_people","harmonic_clear_people_saved_search_net_new_results","harmonic_enrich_person","harmonic_get_company_employees","harmonic_get_email_enrichment_job","harmonic_get_email_enrichment_usage","harmonic_get_enrichment_status","harmonic_get_people_saved_search_net_new_results","harmonic_get_people_saved_search_results","harmonic_get_person","harmonic_list_people_saved_searches","harmonic_search_people_scout","harmonic_submit_email_enrichment_job","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_policy","iam_get_role","iam_get_user","iam_list_access_keys","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","iam_update_access_key","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_describe_group","identity_center_describe_user","identity_center_get_group","identity_center_get_user","identity_center_list_account_assignments","identity_center_list_accounts","identity_center_list_assignments_for_account","identity_center_list_group_memberships","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_get_content_v2","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","lambda_add_permission","lambda_create_alias","lambda_create_event_source_mapping","lambda_create_function","lambda_create_function_url_config","lambda_delete_alias","lambda_delete_event_source_mapping","lambda_delete_function","lambda_delete_function_concurrency","lambda_delete_function_event_invoke_config","lambda_delete_function_url_config","lambda_delete_provisioned_concurrency_config","lambda_get_account_settings","lambda_get_alias","lambda_get_event_source_mapping","lambda_get_function","lambda_get_function_concurrency","lambda_get_function_configuration","lambda_get_function_event_invoke_config","lambda_get_function_recursion_config","lambda_get_function_url_config","lambda_get_layer_version","lambda_get_policy","lambda_get_provisioned_concurrency_config","lambda_get_runtime_management_config","lambda_invoke","lambda_list_aliases","lambda_list_event_source_mappings","lambda_list_function_event_invoke_configs","lambda_list_function_url_configs","lambda_list_functions","lambda_list_layer_versions","lambda_list_layers","lambda_list_provisioned_concurrency_configs","lambda_list_tags","lambda_list_versions_by_function","lambda_publish_version","lambda_put_function_concurrency","lambda_put_function_event_invoke_config","lambda_put_function_recursion_config","lambda_put_provisioned_concurrency_config","lambda_put_runtime_management_config","lambda_remove_permission","lambda_tag_resource","lambda_untag_resource","lambda_update_alias","lambda_update_event_source_mapping","lambda_update_function_code","lambda_update_function_configuration","lambda_update_function_url_config","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","manageengine_sdp_add_change_note","manageengine_sdp_add_problem_note","manageengine_sdp_add_request_note","manageengine_sdp_create_asset","manageengine_sdp_create_change","manageengine_sdp_create_problem","manageengine_sdp_create_request","manageengine_sdp_create_solution","manageengine_sdp_delete_asset","manageengine_sdp_delete_change","manageengine_sdp_delete_problem","manageengine_sdp_delete_request","manageengine_sdp_delete_solution","manageengine_sdp_get_asset","manageengine_sdp_get_change","manageengine_sdp_get_problem","manageengine_sdp_get_request","manageengine_sdp_get_solution","manageengine_sdp_list_assets","manageengine_sdp_list_change_notes","manageengine_sdp_list_changes","manageengine_sdp_list_problem_notes","manageengine_sdp_list_problems","manageengine_sdp_list_request_notes","manageengine_sdp_list_requests","manageengine_sdp_list_solutions","manageengine_sdp_update_asset","manageengine_sdp_update_change","manageengine_sdp_update_problem","manageengine_sdp_update_request","manageengine_sdp_update_solution","mcp_list_operations","mcp_run_operation","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_download_file_v2","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_dynamics_365_close_case","microsoft_dynamics_365_close_opportunity","microsoft_dynamics_365_create_record","microsoft_dynamics_365_get_record","microsoft_dynamics_365_list_records","microsoft_dynamics_365_qualify_lead","microsoft_dynamics_365_search_records","microsoft_dynamics_365_update_record","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","microsoft_word_append","microsoft_word_create","microsoft_word_create_from_template","microsoft_word_export_pdf","microsoft_word_list","microsoft_word_read","microsoft_word_replace_text","microsoft_word_update","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","modal_call_function","modal_chat_completion","modal_list_models","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","quickbooks_add_attachment","quickbooks_create_bill","quickbooks_create_bill_payment","quickbooks_create_credit_memo","quickbooks_create_customer","quickbooks_create_customer_payment","quickbooks_create_deposit","quickbooks_create_employee","quickbooks_create_estimate","quickbooks_create_invoice","quickbooks_create_item","quickbooks_create_journal_entry","quickbooks_create_purchase","quickbooks_create_purchase_order","quickbooks_create_refund_receipt","quickbooks_create_sales_receipt","quickbooks_create_vendor","quickbooks_create_vendor_credit","quickbooks_download_attachment","quickbooks_download_transaction_pdf","quickbooks_email_transaction","quickbooks_get_company_info","quickbooks_read_accounting_transactions","quickbooks_read_attachments","quickbooks_read_master_data","quickbooks_read_purchasing_transactions","quickbooks_read_sales_transactions","quickbooks_run_financial_report","quickbooks_update_bill","quickbooks_update_bill_payment","quickbooks_update_credit_memo","quickbooks_update_customer","quickbooks_update_customer_payment","quickbooks_update_deposit","quickbooks_update_employee","quickbooks_update_estimate","quickbooks_update_invoice","quickbooks_update_item","quickbooks_update_journal_entry","quickbooks_update_purchase","quickbooks_update_purchase_order","quickbooks_update_refund_receipt","quickbooks_update_sales_receipt","quickbooks_update_vendor","quickbooks_update_vendor_credit","quickbooks_void_bill_payment","quickbooks_void_customer_payment","quickbooks_void_invoice","quickbooks_void_sales_receipt","quiver_image_to_svg","quiver_image_to_svg_v2","quiver_list_models","quiver_text_to_svg","quiver_text_to_svg_v2","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","sailpoint_approve_access_request","sailpoint_cancel_access_request","sailpoint_decide_certification_review_items","sailpoint_get_access_profile","sailpoint_get_access_profile_entitlements","sailpoint_get_access_request_config","sailpoint_get_access_request_status","sailpoint_get_account","sailpoint_get_account_activity","sailpoint_get_account_entitlements","sailpoint_get_account_selections","sailpoint_get_campaign","sailpoint_get_certification","sailpoint_get_entitlement","sailpoint_get_entitlement_request_config","sailpoint_get_identity","sailpoint_get_role","sailpoint_get_role_entitlements","sailpoint_get_source","sailpoint_get_task_status","sailpoint_list_access_profiles","sailpoint_list_account_activities","sailpoint_list_accounts","sailpoint_list_campaigns","sailpoint_list_certification_review_items","sailpoint_list_certifications","sailpoint_list_entitlements","sailpoint_list_identities","sailpoint_list_identity_entitlements","sailpoint_list_pending_access_request_approvals","sailpoint_list_roles","sailpoint_list_sources","sailpoint_load_accounts","sailpoint_load_entitlements","sailpoint_reject_access_request","sailpoint_request_access","sailpoint_search","sailpoint_search_aggregate","sailpoint_search_count","sailpoint_sign_off_certification","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","semrush_backlinks","semrush_backlinks_anchors","semrush_backlinks_competitors","semrush_backlinks_geo_distribution","semrush_backlinks_indexed_pages","semrush_backlinks_overview","semrush_backlinks_tld_distribution","semrush_batch_keyword_overview","semrush_broad_match_keywords","semrush_domain_ad_copies","semrush_domain_ad_history","semrush_domain_organic_competitors","semrush_domain_organic_keywords","semrush_domain_overview","semrush_domain_overview_all","semrush_domain_overview_history","semrush_domain_paid_competitors","semrush_domain_paid_keywords","semrush_domain_pla_copies","semrush_domain_pla_keywords","semrush_domain_vs_domain","semrush_keyword_ad_history","semrush_keyword_difficulty","semrush_keyword_overview","semrush_keyword_overview_all","semrush_keyword_questions","semrush_organic_results","semrush_paid_results","semrush_referring_domains","semrush_referring_ips","semrush_related_keywords","semrush_subdomain_ad_copies","semrush_subdomain_organic_keywords","semrush_subdomain_overview","semrush_subdomain_overview_all","semrush_subdomain_overview_history","semrush_subdomain_paid_keywords","semrush_top_domains","semrush_url_organic_keywords","semrush_url_overview","semrush_url_overview_all","semrush_url_overview_history","semrush_url_paid_keywords","semrush_winners_and_losers","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_download_attachment_v2","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_download_v2","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_agent_session_v2","slack_rename_conversation","slack_schedule_message","slack_set_agent_session_status_v2","slack_set_conversation_purpose","slack_set_conversation_topic","slack_set_status","slack_set_suggested_prompts","slack_set_suggested_prompts_v2","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_cancel_message_move_task","sqs_change_message_visibility","sqs_change_message_visibility_batch","sqs_create_queue","sqs_delete_message","sqs_delete_message_batch","sqs_delete_queue","sqs_get_queue_attributes","sqs_get_queue_url","sqs_list_dead_letter_source_queues","sqs_list_message_move_tasks","sqs_list_queue_tags","sqs_list_queues","sqs_purge_queue","sqs_receive_message","sqs_send","sqs_send_message_batch","sqs_set_queue_attributes","sqs_start_message_move_task","sqs_tag_queue","sqs_untag_queue","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_download_file_v2","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","ssm_cancel_command","ssm_delete_parameter","ssm_describe_automation_executions","ssm_describe_instance_information","ssm_describe_instance_patch_states","ssm_describe_instance_patches","ssm_describe_parameters","ssm_get_automation_execution","ssm_get_command_invocation","ssm_get_document","ssm_get_parameter","ssm_get_parameters","ssm_get_parameters_by_path","ssm_list_command_invocations","ssm_list_commands","ssm_list_compliance_items","ssm_list_compliance_summaries","ssm_list_documents","ssm_put_parameter","ssm_send_command","ssm_start_automation_execution","ssm_stop_automation_execution","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","tinyfish_cancel_run","tinyfish_fetch","tinyfish_get_run","tinyfish_list_profiles","tinyfish_list_runs","tinyfish_list_vault_items","tinyfish_run","tinyfish_run_async","tinyfish_search","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 275e8594c99..dacfa485f2a 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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"affinity_batch_update_entity_fields":{"id":"affinity_batch_update_entity_fields","name":"Affinity Batch Update Entity Fields","description":"Write up to 100 non-list field values on one company or person in a single request.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to write the fields on: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"updates":{"type":"json","required":true,"visibility":"user-or-llm","description":"Up to 100 field updates as [{\\"id\\":\\"\\",\\"value\\":{\\"type\\":\\"…\\",\\"data\\":…}}], using the same value shapes as a single field update"}},"hostedApiKey":"none"},"affinity_batch_update_list_entry_fields":{"id":"affinity_batch_update_list_entry_fields","name":"Affinity Batch Update List Entry Fields","description":"Write up to 100 field values on one list row in a single request. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"updates":{"type":"json","required":true,"visibility":"user-or-llm","description":"Up to 100 field updates as [{\\"id\\":\\"\\",\\"value\\":{\\"type\\":\\"…\\",\\"data\\":…}}], using the same value shapes as a single field update"}},"hostedApiKey":"none"},"affinity_create_list":{"id":"affinity_create_list","name":"Affinity Create List","description":"Create a list. Its type fixes which entities it can hold, and the API key holder becomes its creator and owner.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the new list"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Entity kind the list holds: company, opportunity, or person"},"isPublic":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether everyone in the organization can see the list"}},"hostedApiKey":"none"},"affinity_create_list_field_dropdown_option":{"id":"affinity_create_list_field_dropdown_option","name":"Affinity Create List Field Dropdown Option","description":"Add a selectable option to a dropdown field on a list. A ranked or status option also needs a rank and a color.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Kind of option to create, matching the field. dropdown takes only a label; ranked-dropdown also requires rank and color; status-dropdown additionally requires a status category. Sending a field the kind does not accept is rejected"},"text":{"type":"string","required":true,"visibility":"user-or-llm","description":"The option label"},"rank":{"type":"number","required":false,"visibility":"user-or-llm","description":"Sort order. Required on a ranked-dropdown or status-dropdown option"},"color":{"type":"string","required":false,"visibility":"user-or-llm","description":"Option color: white, gray, blue, green, purple, orange, or red. Required on a ranked-dropdown or status-dropdown option"},"statusCategory":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pipeline meaning of the option: open, won, lost, or on-hold. Status-dropdown options only"},"winRate":{"type":"number","required":false,"visibility":"user-or-llm","description":"Expected win rate of the status. Status-dropdown options only"}},"hostedApiKey":"none"},"affinity_create_merge":{"id":"affinity_create_merge","name":"Affinity Create Merge","description":"Fold a duplicate company or person into the record you are keeping. The merge runs asynchronously — poll the returned task to see it finish. Requires the \\"Manage duplicates\\" permission and an admin role.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to merge: companies or persons"},"primaryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to keep"},"duplicateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the duplicate record to fold in"}},"hostedApiKey":"none"},"affinity_create_note":{"id":"affinity_create_note","name":"Affinity Create Note","description":"Write a note — attached to companies, persons, and opportunities, anchored to a meeting, call, or chat message, or posted as a reply to an existing note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Note shape: entities to attach it to records, interaction to anchor it to a meeting, call, or chat message, or user-reply to reply to a note"},"html":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note body as HTML"},"companyIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Companies to attach the note to, e.g. [1, 2]. Not used on a reply"},"personIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Persons to attach the note to, e.g. [1, 2]. Not used on a reply"},"opportunityIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Opportunities to attach the note to, e.g. [1, 2]. Not used on a reply"},"interactionId":{"type":"string","required":false,"visibility":"user-or-llm","description":"The interaction to anchor the note to. Required for an interaction note"},"interactionType":{"type":"string","required":false,"visibility":"user-or-llm","description":"Kind of the anchoring interaction: meeting, call, or chat-message. Required for an interaction note"},"parentId":{"type":"string","required":false,"visibility":"user-or-llm","description":"The note being replied to. Required for a user-reply note"},"creatorId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Attribute the note to another internal person. Defaults to the API key holder"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"Backdate the note to this ISO 8601 timestamp"}},"hostedApiKey":"none"},"affinity_create_reminder":{"id":"affinity_create_reminder","name":"Affinity Create Reminder","description":"Create a reminder on one company, person, or opportunity. A recurring reminder resets whenever the chosen signal happens instead of firing once.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"one-time to fire once, or recurring to reset on a signal"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"What the reminder is about: company, person, or opportunity"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company, person, or opportunity"},"dueDate":{"type":"string","required":false,"visibility":"user-or-llm","description":"When the reminder is due, as an ISO 8601 timestamp. Required for a one-time reminder; on a recurring one Affinity computes it from the period when omitted"},"content":{"type":"string","required":false,"visibility":"user-or-llm","description":"What the reminder says"},"ownerId":{"type":"string","required":true,"visibility":"user-or-llm","description":"User the reminder is assigned to. Must be an internal user. The API key holder is recorded as the creator, which is a separate field"},"resetTrigger":{"type":"string","required":false,"visibility":"user-or-llm","description":"What restarts a recurring reminder: interaction, email, or event. Required when the type is recurring"},"periodDays":{"type":"number","required":false,"visibility":"user-or-llm","description":"Days between firings of a recurring reminder. Required when the type is recurring"}},"hostedApiKey":"none"},"affinity_delete_list_field_dropdown_option":{"id":"affinity_delete_list_field_dropdown_option","name":"Affinity Delete List Field Dropdown Option","description":"Permanently delete a dropdown option on a list field. Every list entry currently set to it is cleared, and those values cannot be recovered.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID to delete"}},"hostedApiKey":"none"},"affinity_delete_note":{"id":"affinity_delete_note","name":"Affinity Delete Note","description":"Delete a note you created. Deleting a root note also deletes its replies; deleting a reply removes only that reply.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID to delete"}},"hostedApiKey":"none"},"affinity_get_company":{"id":"affinity_get_company","name":"Affinity Get Company","description":"Look up one company by ID. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"companyId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The company ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_current_user":{"id":"affinity_get_current_user","name":"Affinity Get Current User","description":"Verify an Affinity API key and return the tenant, the user behind the key, and the scopes the grant carries.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"}},"hostedApiKey":"none"},"affinity_get_entity_field_value":{"id":"affinity_get_entity_field_value","name":"Affinity Get Entity Field Value","description":"Read one non-list field value from a company or person.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to read the field from: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to read"}},"hostedApiKey":"none"},"affinity_get_list":{"id":"affinity_get_list","name":"Affinity Get List","description":"Read one list — its name, type, owner, and privacy setting.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"}},"hostedApiKey":"none"},"affinity_get_list_entry":{"id":"affinity_get_list_entry","name":"Affinity Get List Entry","description":"Read one row of a list with its entity. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_list_entry_field":{"id":"affinity_get_list_entry_field","name":"Affinity Get List Entry Field","description":"Read one field value on a list row.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to read"}},"hostedApiKey":"none"},"affinity_get_list_field_dropdown_option":{"id":"affinity_get_list_field_dropdown_option","name":"Affinity Get List Field Dropdown Option","description":"Read one dropdown option on a list field.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID"}},"hostedApiKey":"none"},"affinity_get_merge":{"id":"affinity_get_merge","name":"Affinity Get Merge","description":"Read the status of one company or person merge, including why it failed if it did.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge to read: companies or persons"},"mergeId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The merge ID"}},"hostedApiKey":"none"},"affinity_get_merge_task":{"id":"affinity_get_merge_task","name":"Affinity Get Merge Task","description":"Read one merge task and how its merges are progressing. Poll this after starting a merge.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge task to read: companies or persons"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The merge task ID"}},"hostedApiKey":"none"},"affinity_get_note":{"id":"affinity_get_note","name":"Affinity Get Note","description":"Read one note with its body, author, mentions, and attached records.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return, e.g. [\\"repliesCount\\",\\"personsPreview\\",\\"companiesPreview\\",\\"opportunitiesPreview\\"]. Those four fields are omitted unless requested here"}},"hostedApiKey":"none"},"affinity_get_opportunity":{"id":"affinity_get_opportunity","name":"Affinity Get Opportunity","description":"Read one opportunity and the list it belongs to. Its field data lives on the list entry.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"opportunityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The opportunity ID"}},"hostedApiKey":"none"},"affinity_get_person":{"id":"affinity_get_person","name":"Affinity Get Person","description":"Look up one person by ID. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"personId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The person ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_saved_view":{"id":"affinity_get_saved_view","name":"Affinity Get Saved View","description":"Read one saved view — its name, kind, and creation date.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"viewId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The saved view ID"}},"hostedApiKey":"none"},"affinity_get_transcript":{"id":"affinity_get_transcript","name":"Affinity Get Transcript","description":"Read one transcript with its first 100 fragments. Page the fragments endpoint for a longer meeting.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"transcriptId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The transcript ID"}},"hostedApiKey":"none"},"affinity_get_user":{"id":"affinity_get_user","name":"Affinity Get User","description":"Read one internal user. A user and their person record share the same numeric ID, so a person ID works here.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"userId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The user ID, which is also their person ID"}},"hostedApiKey":"none"},"affinity_list_calls":{"id":"affinity_list_calls","name":"Affinity List Calls","description":"Page through logged calls and their participants. Only calls the API key holder can see are returned.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_chat_messages":{"id":"affinity_list_chat_messages","name":"Affinity List Chat Messages","description":"Page through logged chat messages and their participants. Only messages the API key holder can see are returned.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_companies":{"id":"affinity_list_companies","name":"Affinity List Companies","description":"Page through companies. Companies come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these company IDs, e.g. [1, 2, 3]"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_coworker_connections":{"id":"affinity_list_coworker_connections","name":"Affinity List Coworker Connections","description":"Find warm paths into a company through shared work history: who in your Affinity data once worked alongside the people you want to reach. Grouped by target, strongest first.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":true,"visibility":"user-or-llm","description":"Required scope. The only supported filter is target.currentCompany.id, e.g. \\"target.currentCompany.id=123\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of targets to return per page, 1-50. Defaults to 20"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_emails":{"id":"affinity_list_emails","name":"Affinity List Emails","description":"Page through email metadata — subject, participants, and timestamps. Affinity never exposes email bodies through the API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_field_values":{"id":"affinity_list_entity_field_values","name":"Affinity List Entity Field Values","description":"Page through a company\'s or person\'s non-list field values. List fields are not returned here — read those through the list entry.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to read field values from: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field IDs. Mutually exclusive with Field Types"},"types":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field categories: enriched, global, relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_list_entity_list_entries":{"id":"affinity_list_entity_list_entries","name":"Affinity List Entity List Entries","description":"Page through a company\'s or person\'s rows across every list, each carrying that list\'s field values and when the entity was added.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up the rows of: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_lists":{"id":"affinity_list_entity_lists","name":"Affinity List Entity Lists","description":"List every list a company or person appears on that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up the lists of: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_notes":{"id":"affinity_list_entity_notes","name":"Affinity List Entity Notes","description":"List the notes relevant to one company, person, or opportunity — directly attached notes plus notes reaching it through its people and meetings.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity the notes hang off: companies, persons, or opportunities"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company, person, or opportunity"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_entity_relationships":{"id":"affinity_list_entity_relationships","name":"Affinity List Entity Relationships","description":"List who knows a company or person, scored 0.0 to 1.0 by how much the two actually interact. Strongest first by default.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up relationships for: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on interactionScore only, e.g. \\"interactionScore>=0.5\\""},"orderBy":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order: [\\"interactionScore\\"] for weakest first, [\\"-interactionScore\\"] for strongest first (the default)"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_field_dropdown_options":{"id":"affinity_list_field_dropdown_options","name":"Affinity List Field Dropdown Options","description":"List the selectable options on a dropdown or ranked-dropdown company or person field. Writing such a field needs the option ID, not its text.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which field family the field belongs to: companies or persons"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown or ranked-dropdown field ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_field_metadata":{"id":"affinity_list_field_metadata","name":"Affinity List Field Metadata","description":"List the non-list company or person fields, with the value type, filter operators, and sort support of each. Start here to find the Field IDs the read and write tools take.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which fields to describe: companies or persons"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return: [\\"filterability\\",\\"sortability\\"]. Both are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on name only, e.g. \\"name=~Status\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_field_value_changes":{"id":"affinity_list_field_value_changes","name":"Affinity List Field Value Changes","description":"Page through field value changes across the whole workspace. Built for delta sync: follow nextCursor to the end of a run, then resume from the last cursor next time.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over field.id, listEntry.id, changer.id, changedAt, or actionType. Resume a sync with e.g. \\"changedAt>2026-06-01T12:00:00Z\\""},"orderBy":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order: [\\"changedAt\\"] for oldest first (the default), [\\"-changedAt\\"] for newest first"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_investor_executive_connections":{"id":"affinity_list_investor_executive_connections","name":"Affinity List Investor Executive Connections","description":"Find warm paths into a company through investment history: which investors in your Affinity data backed a company the people you want to reach once led. Grouped by target, strongest first.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":true,"visibility":"user-or-llm","description":"Required scope. The only supported filter is target.currentCompany.id, e.g. \\"target.currentCompany.id=123\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of targets to return per page, 1-50. Defaults to 20"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_list_entries":{"id":"affinity_list_list_entries","name":"Affinity List List Entries","description":"Page through the rows of a list. Rows come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_entry_field_value_changes":{"id":"affinity_list_list_entry_field_value_changes","name":"Affinity List List Entry Field Value Changes","description":"Page through the history of one list row — who changed which field, when, and to what.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over field.id, changer.id, changedAt, or actionType, e.g. \\"field.id=field-1234\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_entry_fields":{"id":"affinity_list_list_entry_fields","name":"Affinity List List Entry Fields","description":"Page through every field value on one list row, including the list-specific columns. All fields are returned unless narrowed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field IDs. Mutually exclusive with Field Types"},"types":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field categories: enriched, global, list, relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_list_list_field_dropdown_options":{"id":"affinity_list_list_field_dropdown_options","name":"Affinity List List Field Dropdown Options","description":"List the selectable options on a dropdown, ranked-dropdown, or status-dropdown field of a list.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_fields":{"id":"affinity_list_list_fields","name":"Affinity List List Fields","description":"List the fields available on one list, including its list-specific columns. Use these Field IDs when reading or writing list entries.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return: [\\"filterability\\",\\"sortability\\"]. Both are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on name only, e.g. \\"name=~Stage\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_lists":{"id":"affinity_list_lists","name":"Affinity List Lists","description":"Page through the lists in the organization that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"term":{"type":"string","required":false,"visibility":"user-or-llm","description":"Case-insensitive substring match on the list name"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_meetings":{"id":"affinity_list_meetings","name":"Affinity List Meetings","description":"Page through past and upcoming meetings with their organizer and attendees.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_merge_tasks":{"id":"affinity_list_merge_tasks","name":"Affinity List Merge Tasks","description":"Page through merge tasks, each summarizing how many of its merges are in progress, succeeded, or failed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge tasks to list: companies or persons"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on status only, e.g. \\"status=in-progress\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_merges":{"id":"affinity_list_merges","name":"Affinity List Merges","description":"Page through the company or person merges the organization has run, with the status and the records involved in each.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merges to list: companies or persons"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over status or taskId, e.g. \\"status=failed\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_note_attached_companies":{"id":"affinity_list_note_attached_companies","name":"Affinity List Note Attached Companies","description":"List the companies directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_attached_opportunities":{"id":"affinity_list_note_attached_opportunities","name":"Affinity List Note Attached Opportunities","description":"List the opportunities directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_attached_persons":{"id":"affinity_list_note_attached_persons","name":"Affinity List Note Attached Persons","description":"List the persons directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_replies":{"id":"affinity_list_note_replies","name":"Affinity List Note Replies","description":"Page through the replies on one note, including AI Notetaker replies.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID whose replies to read"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_notes":{"id":"affinity_list_notes","name":"Affinity List Notes","description":"Page through every note the caller can see. Replies are excluded.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return, e.g. [\\"repliesCount\\",\\"personsPreview\\",\\"companiesPreview\\",\\"opportunitiesPreview\\"]. Those four fields are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_opportunities":{"id":"affinity_list_opportunities","name":"Affinity List Opportunities","description":"Page through opportunities. Field data lives on the list entry, not here — read it through the list or saved view the opportunity belongs to.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these opportunity IDs, e.g. [1, 2, 3]"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_persons":{"id":"affinity_list_persons","name":"Affinity List Persons","description":"Page through persons. Persons come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these person IDs, e.g. [1, 2, 3]"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_reminders":{"id":"affinity_list_reminders","name":"Affinity List Reminders","description":"Page through the reminders the caller can see. Filter by status to surface what is overdue.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_saved_view_entries":{"id":"affinity_list_saved_view_entries","name":"Affinity List Saved View Entries","description":"Page through the rows of a saved view. The view\'s own filters and columns decide which rows and which field data come back.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"viewId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The saved view ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_saved_views":{"id":"affinity_list_saved_views","name":"Affinity List Saved Views","description":"List the saved views on a list that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_transcript_fragments":{"id":"affinity_list_transcript_fragments","name":"Affinity List Transcript Fragments","description":"Page through everything said in a meeting, segment by segment with the speaker.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"transcriptId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The transcript ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_transcripts":{"id":"affinity_list_transcripts","name":"Affinity List Transcripts","description":"Page through meeting transcript metadata. Read one transcript to get what was actually said.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_users":{"id":"affinity_list_users","name":"Affinity List Users","description":"Page through the internal users in the organization. Email addresses and roles are returned only to callers with the \\"Manage Users\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"term":{"type":"string","required":false,"visibility":"user-or-llm","description":"Case-insensitive match across first name, last name, and primary email"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over id or status, e.g. \\"status=active\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_search_companies":{"id":"affinity_search_companies","name":"Affinity Search Companies","description":"Search companies by filters, sorts, and a free-text term. Requires the \\"Export All Organizations directory\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_search_files":{"id":"affinity_search_files","name":"Affinity Search Files","description":"Search files by keyword, ordered by relevance. Narrow to specific files or to one company, or leave both unset to search the whole account.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to search for. Between 3 and 500 characters"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to these file IDs. Cannot be combined with Company ID"},"companyId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Restrict the search to one company\'s files. Cannot be combined with file IDs"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of files to return, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_search_list_entries":{"id":"affinity_search_list_entries","name":"Affinity Search List Entries","description":"Search the rows of one list by filters, sorts, and a free-text term. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID to search"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_search_notes":{"id":"affinity_search_notes","name":"Affinity Search Notes","description":"Search notes by keyword, ordered by relevance. Narrow to specific notes or to one company, or leave both unset to search the whole account.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to search for. Between 3 and 500 characters"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to these note IDs. Cannot be combined with Company ID"},"companyId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Restrict the search to one company\'s notes. Cannot be combined with note IDs"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of notes to return, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_search_persons":{"id":"affinity_search_persons","name":"Affinity Search Persons","description":"Search persons by filters, sorts, and a free-text term. Requires the \\"Export All People directory\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_semantic_search":{"id":"affinity_semantic_search","name":"Affinity Semantic Search","description":"Find companies from a description in plain language — industry, technology, stage, or business model. Currently searches companies only.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to look for, in plain language, e.g. \\"climate tech companies in our pipeline\\". Up to 500 characters"},"listIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to companies on these lists, e.g. [1, 2]"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of companies to return, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_update_entity_field_value":{"id":"affinity_update_entity_field_value","name":"Affinity Update Entity Field Value","description":"Write one non-list field value on a company or person. The value type must match how the field is defined.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to write the field on: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to write"},"value":{"type":"json","required":true,"visibility":"user-or-llm","description":"The new value as {type, data}, where type matches the field\'s value type. Examples: {\\"type\\":\\"text\\",\\"data\\":\\"Series B\\"}, {\\"type\\":\\"number\\",\\"data\\":42}, {\\"type\\":\\"dropdown\\",\\"data\\":{\\"dropdownOptionId\\":7}}, {\\"type\\":\\"person\\",\\"data\\":{\\"id\\":123}}, {\\"type\\":\\"person-multi\\",\\"data\\":[{\\"id\\":123}]}. Pass data as null to clear the field"}},"hostedApiKey":"none"},"affinity_update_list_entry_field":{"id":"affinity_update_list_entry_field","name":"Affinity Update List Entry Field","description":"Write one field value on a list row. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to write"},"value":{"type":"json","required":true,"visibility":"user-or-llm","description":"The new value as {type, data}, where type matches the field\'s value type. Examples: {\\"type\\":\\"text\\",\\"data\\":\\"Series B\\"}, {\\"type\\":\\"number\\",\\"data\\":42}, {\\"type\\":\\"dropdown\\",\\"data\\":{\\"dropdownOptionId\\":7}}, {\\"type\\":\\"person\\",\\"data\\":{\\"id\\":123}}, {\\"type\\":\\"person-multi\\",\\"data\\":[{\\"id\\":123}]}. Pass data as null to clear the field"}},"hostedApiKey":"none"},"affinity_update_list_field_dropdown_option":{"id":"affinity_update_list_field_dropdown_option","name":"Affinity Update List Field Dropdown Option","description":"Change a dropdown option on a list field. Every field is optional — supply only what should change, and only fields the option\'s kind actually has.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID to update"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Replacement option label. Supply at least one field to change"},"rank":{"type":"number","required":false,"visibility":"user-or-llm","description":"Sort order. Required on a ranked-dropdown or status-dropdown option"},"color":{"type":"string","required":false,"visibility":"user-or-llm","description":"Option color: white, gray, blue, green, purple, orange, or red. Required on a ranked-dropdown or status-dropdown option"},"statusCategory":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pipeline meaning of the option: open, won, lost, or on-hold. Status-dropdown options only"},"winRate":{"type":"number","required":false,"visibility":"user-or-llm","description":"Expected win rate of the status. Status-dropdown options only"}},"hostedApiKey":"none"},"affinity_update_note":{"id":"affinity_update_note","name":"Affinity Update Note","description":"Rewrite a note\'s body or replace which records it is attached to. Each list of IDs replaces that association wholesale, an empty list clears it, and omitting one leaves it untouched. A note\'s type cannot be changed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID to update"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"Replacement note body as HTML"},"companyIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached companies, e.g. [1, 2]. Send [] to detach every company; omit to leave them unchanged"},"personIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached persons, e.g. [1, 2]. Send [] to detach every person; omit to leave them unchanged"},"opportunityIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached opportunities, e.g. [1, 2]. Send [] to detach every opportunity; omit to leave them unchanged"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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."}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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."}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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\\"})"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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\\")"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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."}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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\\")"}},"hostedApiKey":"none"},"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\\")"}},"hostedApiKey":"none"},"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."}},"hostedApiKey":"none"},"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."}},"hostedApiKey":"none"},"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\\"})"}},"hostedApiKey":"none"},"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."}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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."}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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\\"."}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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\\""}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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."}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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\\")"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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\\"])"}},"hostedApiKey":"none"},"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\\")"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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\\")"}},"hostedApiKey":"none"},"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)"}},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"hostedApiKey":"none"},"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"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"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"}},"hostedApiKey":"none"},"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"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"UUID of the candidate to anonymize"}},"hostedApiKey":"none"},"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"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"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."}},"hostedApiKey":"none"},"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"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"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"},"archiveEmail":{"type":"json","required":false,"visibility":"user-or-llm","description":"Archive email configuration with communicationTemplateId and optional sendAt ISO 8601 timestamp. Pass null or omit to send no archive email."}},"hostedApiKey":"none"},"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"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"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, or FirstPreInterviewScreen (defaults to the 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)"},"applicationHistory":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional documented application history entries to create with the application"}},"hostedApiKey":"none"},"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"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"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\\"]"},"location":{"type":"json","required":false,"visibility":"user-or-llm","description":"Candidate location object with optional city, region, and country"}},"hostedApiKey":"none"},"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"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"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: , , , ,