-
+
+
1. Navigate to your workspace settings
@@ -40,6 +36,10 @@ MCP servers provide collections of tools that your agents can use. Configure the
You can also configure MCP servers directly from the toolbar in an Agent block for quick setup.
+### Refresh Tools
+
+Click **Refresh** on a server to fetch the latest tool schemas and automatically update any agent blocks using those tools with the new parameter definitions.
+
## Using MCP Tools in Agents
Once MCP servers are configured, their tools become available within your agent blocks:
diff --git a/apps/docs/content/docs/en/mcp/meta.json b/apps/docs/content/docs/en/mcp/meta.json
new file mode 100644
index 0000000000..9ee1817875
--- /dev/null
+++ b/apps/docs/content/docs/en/mcp/meta.json
@@ -0,0 +1,5 @@
+{
+ "title": "MCP",
+ "pages": ["index", "deploy-workflows"],
+ "defaultOpen": false
+}
diff --git a/apps/docs/content/docs/en/tools/jira_service_management.mdx b/apps/docs/content/docs/en/tools/jira_service_management.mdx
new file mode 100644
index 0000000000..d6450eb9ff
--- /dev/null
+++ b/apps/docs/content/docs/en/tools/jira_service_management.mdx
@@ -0,0 +1,490 @@
+---
+title: Jira Service Management
+description: Interact with Jira Service Management
+---
+
+import { BlockInfoCard } from "@/components/ui/block-info-card"
+
+
+
+## Usage Instructions
+
+Integrate with Jira Service Management for IT service management. Create and manage service requests, handle customers and organizations, track SLAs, and manage queues.
+
+
+
+## Tools
+
+### `jsm_get_service_desks`
+
+Get all service desks from Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `serviceDesks` | json | Array of service desks |
+| `total` | number | Total number of service desks |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_get_request_types`
+
+Get request types for a service desk in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `serviceDeskId` | string | Yes | Service Desk ID to get request types for |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `requestTypes` | json | Array of request types |
+| `total` | number | Total number of request types |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_create_request`
+
+Create a new service request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `serviceDeskId` | string | Yes | Service Desk ID to create the request in |
+| `requestTypeId` | string | Yes | Request Type ID for the new request |
+| `summary` | string | Yes | Summary/title for the service request |
+| `description` | string | No | Description for the service request |
+| `raiseOnBehalfOf` | string | No | Account ID of customer to raise request on behalf of |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueId` | string | Created request issue ID |
+| `issueKey` | string | Created request issue key \(e.g., SD-123\) |
+| `requestTypeId` | string | Request type ID |
+| `serviceDeskId` | string | Service desk ID |
+| `success` | boolean | Whether the request was created successfully |
+| `url` | string | URL to the created request |
+
+### `jsm_get_request`
+
+Get a single service request from Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+
+### `jsm_get_requests`
+
+Get multiple service requests from Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `serviceDeskId` | string | No | Filter by service desk ID |
+| `requestOwnership` | string | No | Filter by ownership: OWNED_REQUESTS, PARTICIPATED_REQUESTS, ORGANIZATION, ALL_REQUESTS |
+| `requestStatus` | string | No | Filter by status: OPEN, CLOSED, ALL |
+| `searchTerm` | string | No | Search term to filter requests |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `requests` | json | Array of service requests |
+| `total` | number | Total number of requests |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_add_comment`
+
+Add a comment (public or internal) to a service request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+| `body` | string | Yes | Comment body text |
+| `isPublic` | boolean | Yes | Whether the comment is public \(visible to customer\) or internal |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `commentId` | string | Created comment ID |
+| `body` | string | Comment body text |
+| `isPublic` | boolean | Whether the comment is public |
+| `success` | boolean | Whether the comment was added successfully |
+
+### `jsm_get_comments`
+
+Get comments for a service request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+| `isPublic` | boolean | No | Filter to only public comments |
+| `internal` | boolean | No | Filter to only internal comments |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `comments` | json | Array of comments |
+| `total` | number | Total number of comments |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_get_customers`
+
+Get customers for a service desk in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `serviceDeskId` | string | Yes | Service Desk ID to get customers for |
+| `query` | string | No | Search query to filter customers |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `customers` | json | Array of customers |
+| `total` | number | Total number of customers |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_add_customer`
+
+Add customers to a service desk in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `serviceDeskId` | string | Yes | Service Desk ID to add customers to |
+| `emails` | string | Yes | Comma-separated email addresses to add as customers |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `serviceDeskId` | string | Service desk ID |
+| `success` | boolean | Whether customers were added successfully |
+
+### `jsm_get_organizations`
+
+Get organizations for a service desk in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `serviceDeskId` | string | Yes | Service Desk ID to get organizations for |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `organizations` | json | Array of organizations |
+| `total` | number | Total number of organizations |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_create_organization`
+
+Create a new organization in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `name` | string | Yes | Name of the organization to create |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `organizationId` | string | ID of the created organization |
+| `name` | string | Name of the created organization |
+| `success` | boolean | Whether the operation succeeded |
+
+### `jsm_add_organization_to_service_desk`
+
+Add an organization to a service desk in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `serviceDeskId` | string | Yes | Service Desk ID to add the organization to |
+| `organizationId` | string | Yes | Organization ID to add to the service desk |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `serviceDeskId` | string | Service Desk ID |
+| `organizationId` | string | Organization ID added |
+| `success` | boolean | Whether the operation succeeded |
+
+### `jsm_get_queues`
+
+Get queues for a service desk in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `serviceDeskId` | string | Yes | Service Desk ID to get queues for |
+| `includeCount` | boolean | No | Include issue count for each queue |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `queues` | json | Array of queues |
+| `total` | number | Total number of queues |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_get_sla`
+
+Get SLA information for a service request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `slas` | json | Array of SLA information |
+| `total` | number | Total number of SLAs |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_get_transitions`
+
+Get available transitions for a service request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `transitions` | json | Array of available transitions |
+
+### `jsm_transition_request`
+
+Transition a service request to a new status in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+| `transitionId` | string | Yes | Transition ID to apply |
+| `comment` | string | No | Optional comment to add during transition |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `transitionId` | string | Applied transition ID |
+| `success` | boolean | Whether the transition was successful |
+
+### `jsm_get_participants`
+
+Get participants for a request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `participants` | json | Array of participants |
+| `total` | number | Total number of participants |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_add_participants`
+
+Add participants to a request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+| `accountIds` | string | Yes | Comma-separated account IDs to add as participants |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `participants` | json | Array of added participants |
+| `success` | boolean | Whether the operation succeeded |
+
+### `jsm_get_approvals`
+
+Get approvals for a request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+| `start` | number | No | Start index for pagination \(default: 0\) |
+| `limit` | number | No | Maximum results to return \(default: 50\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `approvals` | json | Array of approvals |
+| `total` | number | Total number of approvals |
+| `isLastPage` | boolean | Whether this is the last page |
+
+### `jsm_answer_approval`
+
+Approve or decline an approval request in Jira Service Management
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
+| `cloudId` | string | No | Jira Cloud ID for the instance |
+| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., SD-123\) |
+| `approvalId` | string | Yes | Approval ID to answer |
+| `decision` | string | Yes | Decision: "approve" or "decline" |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Timestamp of the operation |
+| `issueIdOrKey` | string | Issue ID or key |
+| `approvalId` | string | Approval ID |
+| `decision` | string | Decision made \(approve/decline\) |
+| `success` | boolean | Whether the operation succeeded |
+
+
+
+## Notes
+
+- Category: `tools`
+- Type: `jira_service_management`
diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json
index 82b569adf6..7ee6738b7a 100644
--- a/apps/docs/content/docs/en/tools/meta.json
+++ b/apps/docs/content/docs/en/tools/meta.json
@@ -46,6 +46,7 @@
"intercom",
"jina",
"jira",
+ "jira_service_management",
"kalshi",
"knowledge",
"linear",
diff --git a/apps/docs/content/docs/es/mcp/deploy-workflows.mdx b/apps/docs/content/docs/es/mcp/deploy-workflows.mdx
new file mode 100644
index 0000000000..82869165fa
--- /dev/null
+++ b/apps/docs/content/docs/es/mcp/deploy-workflows.mdx
@@ -0,0 +1,108 @@
+---
+title: Implementar flujos de trabajo como MCP
+description: Expone tus flujos de trabajo como herramientas MCP para asistentes
+ de IA externos y aplicaciones
+---
+
+import { Video } from '@/components/ui/video'
+import { Callout } from 'fumadocs-ui/components/callout'
+
+Implementa tus flujos de trabajo como herramientas MCP para hacerlos accesibles a asistentes de IA externos como Claude Desktop, Cursor y otros clientes compatibles con MCP. Esto convierte tus flujos de trabajo en herramientas invocables que pueden ser llamadas desde cualquier lugar.
+
+## Crear y gestionar servidores MCP
+
+Los servidores MCP agrupan tus herramientas de flujo de trabajo. Créalos y gestiόnalos en la configuración del espacio de trabajo:
+
+
+
+
+
+1. Navega a **Configuración → Servidores MCP**
+2. Haz clic en **Crear servidor**
+3. Introduce un nombre y una descripción opcional
+4. Copia la URL del servidor para usarla en tus clientes MCP
+5. Visualiza y gestiona todas las herramientas añadidas al servidor
+
+## Añadir un flujo de trabajo como herramienta
+
+Una vez que tu flujo de trabajo esté implementado, puedes exponerlo como una herramienta MCP:
+
+
+
+
+
+1. Abre tu flujo de trabajo implementado
+2. Haz clic en **Implementar** y ve a la pestaña **MCP**
+3. Configura el nombre y la descripción de la herramienta
+4. Añade descripciones para cada parámetro (ayuda a la IA a entender las entradas)
+5. Selecciona a qué servidores MCP añadirla
+
+
+El flujo de trabajo debe estar implementado antes de poder añadirse como herramienta MCP.
+
+
+## Configuración de la herramienta
+
+### Nombre de la herramienta
+Usa letras minúsculas, números y guiones bajos. El nombre debe ser descriptivo y seguir las convenciones de nomenclatura de MCP (por ejemplo, `search_documents`, `send_email`).
+
+### Descripción
+Escribe una descripción clara de lo que hace la herramienta. Esto ayuda a los asistentes de IA a entender cuándo usar la herramienta.
+
+### Parámetros
+Los campos de formato de entrada de tu flujo de trabajo se convierten en parámetros de herramienta. Añade descripciones a cada parámetro para ayudar a los asistentes de IA a proporcionar valores correctos.
+
+## Conectar clientes MCP
+
+Usa la URL del servidor desde la configuración para conectar aplicaciones externas:
+
+### Claude Desktop
+Añade a tu configuración de Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):
+
+```json
+{
+ "mcpServers": {
+ "my-sim-workflows": {
+ "command": "npx",
+ "args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
+ }
+ }
+}
+```
+
+### Cursor
+Añade la URL del servidor en la configuración MCP de Cursor usando el mismo patrón mcp-remote.
+
+
+Incluye tu encabezado de clave API (`X-API-Key`) para acceso autenticado al usar mcp-remote u otros transportes MCP basados en HTTP.
+
+
+## Gestión del servidor
+
+Desde la vista de detalle del servidor en **Configuración → Servidores MCP**, puedes:
+
+- **Ver herramientas**: consulta todos los flujos de trabajo añadidos a un servidor
+- **Copiar URL**: obtén la URL del servidor para clientes MCP
+- **Añadir flujos de trabajo**: añade más flujos de trabajo desplegados como herramientas
+- **Eliminar herramientas**: elimina flujos de trabajo del servidor
+- **Eliminar servidor**: elimina el servidor completo y todas sus herramientas
+
+## Cómo funciona
+
+Cuando un cliente MCP llama a tu herramienta:
+
+1. La solicitud se recibe en la URL de tu servidor MCP
+2. Sim valida la solicitud y mapea los parámetros a las entradas del flujo de trabajo
+3. El flujo de trabajo desplegado se ejecuta con las entradas proporcionadas
+4. Los resultados se devuelven al cliente MCP
+
+Los flujos de trabajo se ejecutan usando la misma versión de despliegue que las llamadas API, garantizando un comportamiento consistente.
+
+## Requisitos de permisos
+
+| Acción | Permiso requerido |
+|--------|-------------------|
+| Crear servidores MCP | **Admin** |
+| Añadir flujos de trabajo a servidores | **Write** o **Admin** |
+| Ver servidores MCP | **Read**, **Write** o **Admin** |
+| Eliminar servidores MCP | **Admin** |
diff --git a/apps/docs/content/docs/es/mcp/index.mdx b/apps/docs/content/docs/es/mcp/index.mdx
index 823263c1fa..caa7319472 100644
--- a/apps/docs/content/docs/es/mcp/index.mdx
+++ b/apps/docs/content/docs/es/mcp/index.mdx
@@ -1,8 +1,10 @@
---
-title: MCP (Protocolo de Contexto de Modelo)
+title: Uso de herramientas MCP
+description: Conecta herramientas y servicios externos usando el Model Context Protocol
---
import { Image } from '@/components/ui/image'
+import { Video } from '@/components/ui/video'
import { Callout } from 'fumadocs-ui/components/callout'
El Protocolo de Contexto de Modelo ([MCP](https://modelcontextprotocol.com/)) te permite conectar herramientas y servicios externos utilizando un protocolo estandarizado, permitiéndote integrar APIs y servicios directamente en tus flujos de trabajo. Con MCP, puedes ampliar las capacidades de Sim añadiendo integraciones personalizadas que funcionan perfectamente con tus agentes y flujos de trabajo.
@@ -20,14 +22,8 @@ MCP es un estándar abierto que permite a los asistentes de IA conectarse de for
Los servidores MCP proporcionan colecciones de herramientas que tus agentes pueden utilizar. Configúralos en los ajustes del espacio de trabajo:
-
-
+
+
1. Navega a los ajustes de tu espacio de trabajo
@@ -40,14 +36,18 @@ Los servidores MCP proporcionan colecciones de herramientas que tus agentes pued
También puedes configurar servidores MCP directamente desde la barra de herramientas en un bloque de Agente para una configuración rápida.
+### Actualizar herramientas
+
+Haz clic en **Actualizar** en un servidor para obtener los esquemas de herramientas más recientes y actualizar automáticamente cualquier bloque de agente que use esas herramientas con las nuevas definiciones de parámetros.
+
## Uso de herramientas MCP en agentes
-Una vez que los servidores MCP están configurados, sus herramientas estarán disponibles dentro de tus bloques de agente:
+Una vez configurados los servidores MCP, sus herramientas están disponibles dentro de tus bloques de agente:
-El bloque de Herramienta MCP te permite:
+El bloque de herramienta MCP te permite:
- Ejecutar cualquier herramienta MCP configurada directamente
- Pasar parámetros específicos a la herramienta
- Usar la salida de la herramienta en pasos posteriores del flujo de trabajo
- Encadenar múltiples herramientas MCP
-### Cuándo usar Herramienta MCP vs Agente
+### Cuándo usar herramienta MCP vs. agente
-**Usa Agente con herramientas MCP cuando:**
+**Usa agente con herramientas MCP cuando:**
- Quieres que la IA decida qué herramientas usar
-- Necesitas un razonamiento complejo sobre cuándo y cómo usar las herramientas
-- Deseas una interacción en lenguaje natural con las herramientas
+- Necesitas razonamiento complejo sobre cuándo y cómo usar las herramientas
+- Quieres interacción en lenguaje natural con las herramientas
**Usa el bloque Herramienta MCP cuando:**
-- Necesites una ejecución determinista de herramientas
-- Quieras ejecutar una herramienta específica con parámetros conocidos
-- Estés construyendo flujos de trabajo estructurados con pasos predecibles
+- Necesitas una ejecución determinista de herramientas
+- Quieres ejecutar una herramienta específica con parámetros conocidos
+- Estás construyendo flujos de trabajo estructurados con pasos predecibles
## Requisitos de permisos
@@ -99,22 +99,22 @@ La funcionalidad MCP requiere permisos específicos del espacio de trabajo:
|--------|-------------------|
| Configurar servidores MCP en ajustes | **Admin** |
| Usar herramientas MCP en agentes | **Write** o **Admin** |
-| Ver herramientas MCP disponibles | **Read**, **Write**, o **Admin** |
+| Ver herramientas MCP disponibles | **Read**, **Write** o **Admin** |
| Ejecutar bloques de Herramienta MCP | **Write** o **Admin** |
## Casos de uso comunes
### Integración con bases de datos
-Conéctate a bases de datos para consultar, insertar o actualizar datos dentro de tus flujos de trabajo.
+Conecta con bases de datos para consultar, insertar o actualizar datos dentro de tus flujos de trabajo.
### Integraciones de API
-Accede a APIs externas y servicios web que no tienen integraciones incorporadas en Sim.
+Accede a API externas y servicios web que no tienen integraciones integradas en Sim.
### Acceso al sistema de archivos
Lee, escribe y manipula archivos en sistemas de archivos locales o remotos.
### Lógica de negocio personalizada
-Ejecuta scripts o herramientas personalizadas específicas para las necesidades de tu organización.
+Ejecuta scripts o herramientas personalizadas específicas de las necesidades de tu organización.
### Acceso a datos en tiempo real
Obtén datos en vivo de sistemas externos durante la ejecución del flujo de trabajo.
@@ -122,23 +122,23 @@ Obtén datos en vivo de sistemas externos durante la ejecución del flujo de tra
## Consideraciones de seguridad
- Los servidores MCP se ejecutan con los permisos del usuario que los configuró
-- Verifica siempre las fuentes del servidor MCP antes de la instalación
+- Siempre verifica las fuentes de los servidores MCP antes de la instalación
- Usa variables de entorno para datos de configuración sensibles
-- Revisa las capacidades del servidor MCP antes de conceder acceso a los agentes
+- Revisa las capacidades del servidor MCP antes de otorgar acceso a los agentes
## Solución de problemas
### El servidor MCP no aparece
- Verifica que la configuración del servidor sea correcta
-- Comprueba que tienes los permisos necesarios
-- Asegúrate de que el servidor MCP esté en funcionamiento y sea accesible
+- Comprueba que tienes los permisos requeridos
+- Asegúrate de que el servidor MCP esté en ejecución y accesible
### Fallos en la ejecución de herramientas
- Verifica que los parámetros de la herramienta estén correctamente formateados
-- Revisa los registros del servidor MCP para ver mensajes de error
+- Revisa los registros del servidor MCP para mensajes de error
- Asegúrate de que la autenticación requerida esté configurada
### Errores de permisos
-- Confirma tu nivel de permisos en el espacio de trabajo
-- Comprueba si el servidor MCP requiere autenticación adicional
-- Verifica que el servidor esté configurado correctamente para tu espacio de trabajo
\ No newline at end of file
+- Confirma tu nivel de permisos del espacio de trabajo
+- Verifica si el servidor MCP requiere autenticación adicional
+- Comprueba que el servidor esté configurado correctamente para tu espacio de trabajo
\ No newline at end of file
diff --git a/apps/docs/content/docs/es/tools/jira_service_management.mdx b/apps/docs/content/docs/es/tools/jira_service_management.mdx
new file mode 100644
index 0000000000..cc89d5a093
--- /dev/null
+++ b/apps/docs/content/docs/es/tools/jira_service_management.mdx
@@ -0,0 +1,486 @@
+---
+title: Jira Service Management
+description: Interactúa con Jira Service Management
+---
+
+import { BlockInfoCard } from "@/components/ui/block-info-card"
+
+
+
+## Instrucciones de uso
+
+Integra con Jira Service Management para la gestión de servicios de TI. Crea y gestiona solicitudes de servicio, maneja clientes y organizaciones, realiza seguimiento de SLA y administra colas.
+
+## Herramientas
+
+### `jsm_get_service_desks`
+
+Obtiene todos los service desks de Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `serviceDesks` | json | Array de service desks |
+| `total` | number | Número total de service desks |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_get_request_types`
+
+Obtiene los tipos de solicitud para un service desk en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `serviceDeskId` | string | Sí | ID del service desk para obtener tipos de solicitud |
+| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `requestTypes` | json | Array de tipos de solicitud |
+| `total` | number | Número total de tipos de solicitud |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_create_request`
+
+Crear una nueva solicitud de servicio en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `serviceDeskId` | string | Sí | ID del Service Desk en el que crear la solicitud |
+| `requestTypeId` | string | Sí | ID del tipo de solicitud para la nueva solicitud |
+| `summary` | string | Sí | Resumen/título para la solicitud de servicio |
+| `description` | string | No | Descripción para la solicitud de servicio |
+| `raiseOnBehalfOf` | string | No | ID de cuenta del cliente para crear la solicitud en su nombre |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueId` | string | ID de la incidencia de solicitud creada |
+| `issueKey` | string | Clave de la incidencia de solicitud creada \(p. ej., SD-123\) |
+| `requestTypeId` | string | ID del tipo de solicitud |
+| `serviceDeskId` | string | ID del service desk |
+| `success` | boolean | Si la solicitud se creó correctamente |
+| `url` | string | URL de la solicitud creada |
+
+### `jsm_get_request`
+
+Obtener una única solicitud de servicio de Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+
+### `jsm_get_requests`
+
+Obtener múltiples solicitudes de servicio de Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `serviceDeskId` | string | No | Filtrar por ID de service desk |
+| `requestOwnership` | string | No | Filtrar por propiedad: OWNED_REQUESTS, PARTICIPATED_REQUESTS, ORGANIZATION, ALL_REQUESTS |
+| `requestStatus` | string | No | Filtrar por estado: OPEN, CLOSED, ALL |
+| `searchTerm` | string | No | Término de búsqueda para filtrar solicitudes |
+| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `requests` | json | Array de solicitudes de servicio |
+| `total` | number | Número total de solicitudes |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_add_comment`
+
+Añadir un comentario (público o interno) a una solicitud de servicio en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(ej., SD-123\) |
+| `body` | string | Sí | Texto del cuerpo del comentario |
+| `isPublic` | boolean | Sí | Si el comentario es público \(visible para el cliente\) o interno |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave de la incidencia |
+| `commentId` | string | ID del comentario creado |
+| `body` | string | Texto del cuerpo del comentario |
+| `isPublic` | boolean | Si el comentario es público |
+| `success` | boolean | Si el comentario se añadió correctamente |
+
+### `jsm_get_comments`
+
+Obtener comentarios de una solicitud de servicio en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(ej., SD-123\) |
+| `isPublic` | boolean | No | Filtrar solo comentarios públicos |
+| `internal` | boolean | No | Filtrar solo comentarios internos |
+| `start` | number | No | Índice de inicio para la paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave del issue |
+| `comments` | json | Array de comentarios |
+| `total` | number | Número total de comentarios |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_get_customers`
+
+Obtener clientes para un service desk en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `serviceDeskId` | string | Sí | ID del service desk para obtener clientes |
+| `query` | string | No | Consulta de búsqueda para filtrar clientes |
+| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `customers` | json | Array de clientes |
+| `total` | number | Número total de clientes |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_add_customer`
+
+Añadir clientes a un service desk en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `serviceDeskId` | string | Sí | ID del Service Desk al que añadir clientes |
+| `emails` | string | Sí | Direcciones de correo electrónico separadas por comas para añadir como clientes |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `serviceDeskId` | string | ID del service desk |
+| `success` | boolean | Si los clientes se añadieron correctamente |
+
+### `jsm_get_organizations`
+
+Obtener organizaciones de un service desk en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `serviceDeskId` | string | Sí | ID del Service Desk del que obtener organizaciones |
+| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `organizations` | json | Array de organizaciones |
+| `total` | number | Número total de organizaciones |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_create_organization`
+
+Crear una nueva organización en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `name` | string | Sí | Nombre de la organización a crear |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `organizationId` | string | ID de la organización creada |
+| `name` | string | Nombre de la organización creada |
+| `success` | boolean | Si la operación tuvo éxito |
+
+### `jsm_add_organization_to_service_desk`
+
+Añadir una organización a un service desk en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `serviceDeskId` | string | Sí | ID del service desk al que añadir la organización |
+| `organizationId` | string | Sí | ID de la organización a añadir al service desk |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `serviceDeskId` | string | ID del service desk |
+| `organizationId` | string | ID de la organización añadida |
+| `success` | boolean | Si la operación tuvo éxito |
+
+### `jsm_get_queues`
+
+Obtener colas para un service desk en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `serviceDeskId` | string | Sí | ID del service desk para obtener colas |
+| `includeCount` | boolean | No | Incluir recuento de incidencias para cada cola |
+| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `queues` | json | Array de colas |
+| `total` | number | Número total de colas |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_get_sla`
+
+Obtener información de SLA para una solicitud de servicio en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(ej., SD-123\) |
+| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave de la incidencia |
+| `slas` | json | Array de información de SLA |
+| `total` | number | Número total de SLA |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_get_transitions`
+
+Obtener las transiciones disponibles para una solicitud de servicio en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave de la incidencia |
+| `transitions` | json | Array de transiciones disponibles |
+
+### `jsm_transition_request`
+
+Transicionar una solicitud de servicio a un nuevo estado en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
+| `transitionId` | string | Sí | ID de transición a aplicar |
+| `comment` | string | No | Comentario opcional para añadir durante la transición |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave de la incidencia |
+| `transitionId` | string | ID de transición aplicada |
+| `success` | boolean | Si la transición fue exitosa |
+
+### `jsm_get_participants`
+
+Obtener participantes de una solicitud en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
+| `start` | number | No | Índice de inicio para paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave de la incidencia |
+| `participants` | json | Array de participantes |
+| `total` | number | Número total de participantes |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_add_participants`
+
+Agregar participantes a una solicitud en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
+| `accountIds` | string | Sí | IDs de cuenta separados por comas para agregar como participantes |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave de la incidencia |
+| `participants` | json | Array de participantes añadidos |
+| `success` | boolean | Si la operación tuvo éxito |
+
+### `jsm_get_approvals`
+
+Obtener aprobaciones para una solicitud en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
+| `start` | number | No | Índice de inicio para la paginación \(predeterminado: 0\) |
+| `limit` | number | No | Máximo de resultados a devolver \(predeterminado: 50\) |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave de la incidencia |
+| `approvals` | json | Array de aprobaciones |
+| `total` | number | Número total de aprobaciones |
+| `isLastPage` | boolean | Si esta es la última página |
+
+### `jsm_answer_approval`
+
+Aprobar o rechazar una solicitud de aprobación en Jira Service Management
+
+#### Entrada
+
+| Parámetro | Tipo | Requerido | Descripción |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
+| `cloudId` | string | No | ID de Jira Cloud para la instancia |
+| `issueIdOrKey` | string | Sí | ID o clave de la incidencia \(p. ej., SD-123\) |
+| `approvalId` | string | Sí | ID de aprobación a responder |
+| `decision` | string | Sí | Decisión: "approve" o "decline" |
+
+#### Salida
+
+| Parámetro | Tipo | Descripción |
+| --------- | ---- | ----------- |
+| `ts` | string | Marca de tiempo de la operación |
+| `issueIdOrKey` | string | ID o clave de la incidencia |
+| `approvalId` | string | ID de aprobación |
+| `decision` | string | Decisión tomada \(aprobar/rechazar\) |
+| `success` | boolean | Si la operación tuvo éxito |
+
+## Notas
+
+- Categoría: `tools`
+- Tipo: `jira_service_management`
diff --git a/apps/docs/content/docs/fr/mcp/deploy-workflows.mdx b/apps/docs/content/docs/fr/mcp/deploy-workflows.mdx
new file mode 100644
index 0000000000..61ecb33a7f
--- /dev/null
+++ b/apps/docs/content/docs/fr/mcp/deploy-workflows.mdx
@@ -0,0 +1,108 @@
+---
+title: Déployer des workflows en tant que MCP
+description: Exposez vos workflows en tant qu'outils MCP pour les assistants IA
+ externes et les applications
+---
+
+import { Video } from '@/components/ui/video'
+import { Callout } from 'fumadocs-ui/components/callout'
+
+Déployez vos workflows en tant qu'outils MCP pour les rendre accessibles aux assistants IA externes comme Claude Desktop, Cursor et autres clients compatibles MCP. Cela transforme vos workflows en outils appelables qui peuvent être invoqués depuis n'importe où.
+
+## Créer et gérer des serveurs MCP
+
+Les serveurs MCP regroupent vos outils de workflow. Créez-les et gérez-les dans les paramètres de l'espace de travail :
+
+
+
+
+
+1. Accédez à **Paramètres → Serveurs MCP**
+2. Cliquez sur **Créer un serveur**
+3. Saisissez un nom et une description facultative
+4. Copiez l'URL du serveur pour l'utiliser dans vos clients MCP
+5. Consultez et gérez tous les outils ajoutés au serveur
+
+## Ajouter un workflow en tant qu'outil
+
+Une fois votre workflow déployé, vous pouvez l'exposer en tant qu'outil MCP :
+
+
+
+
+
+1. Ouvrez votre workflow déployé
+2. Cliquez sur **Déployer** et accédez à l'onglet **MCP**
+3. Configurez le nom et la description de l'outil
+4. Ajoutez des descriptions pour chaque paramètre (aide l'IA à comprendre les entrées)
+5. Sélectionnez les serveurs MCP auxquels l'ajouter
+
+
+Le workflow doit être déployé avant de pouvoir être ajouté en tant qu'outil MCP.
+
+
+## Configuration de l'outil
+
+### Nom de l'outil
+Utilisez des lettres minuscules, des chiffres et des traits de soulignement. Le nom doit être descriptif et suivre les conventions de nommage MCP (par exemple, `search_documents`, `send_email`).
+
+### Description
+Rédigez une description claire de ce que fait l'outil. Cela aide les assistants IA à comprendre quand utiliser l'outil.
+
+### Paramètres
+Les champs du format d'entrée de votre workflow deviennent des paramètres d'outil. Ajoutez des descriptions à chaque paramètre pour aider les assistants IA à fournir les valeurs correctes.
+
+## Connexion des clients MCP
+
+Utilisez l'URL du serveur depuis les paramètres pour connecter des applications externes :
+
+### Claude Desktop
+Ajoutez à votre configuration Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`) :
+
+```json
+{
+ "mcpServers": {
+ "my-sim-workflows": {
+ "command": "npx",
+ "args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
+ }
+ }
+}
+```
+
+### Cursor
+Ajoutez l'URL du serveur dans les paramètres MCP de Cursor en utilisant le même modèle mcp-remote.
+
+
+Incluez votre en-tête de clé API (`X-API-Key`) pour un accès authentifié lors de l'utilisation de mcp-remote ou d'autres transports MCP basés sur HTTP.
+
+
+## Gestion du serveur
+
+Depuis la vue détaillée du serveur dans **Paramètres → Serveurs MCP**, vous pouvez :
+
+- **Voir les outils** : voir tous les workflows ajoutés à un serveur
+- **Copier l'URL** : obtenir l'URL du serveur pour les clients MCP
+- **Ajouter des workflows** : ajouter d'autres workflows déployés comme outils
+- **Supprimer des outils** : retirer des workflows du serveur
+- **Supprimer le serveur** : supprimer l'intégralité du serveur et tous ses outils
+
+## Comment ça fonctionne
+
+Lorsqu'un client MCP appelle votre outil :
+
+1. La requête est reçue à l'URL de votre serveur MCP
+2. Sim valide la requête et mappe les paramètres aux entrées du workflow
+3. Le workflow déployé s'exécute avec les entrées fournies
+4. Les résultats sont renvoyés au client MCP
+
+Les workflows s'exécutent en utilisant la même version de déploiement que les appels API, garantissant un comportement cohérent.
+
+## Exigences de permission
+
+| Action | Permission requise |
+|--------|-------------------|
+| Créer des serveurs MCP | **Admin** |
+| Ajouter des workflows aux serveurs | **Write** ou **Admin** |
+| Voir les serveurs MCP | **Read**, **Write** ou **Admin** |
+| Supprimer des serveurs MCP | **Admin** |
diff --git a/apps/docs/content/docs/fr/mcp/index.mdx b/apps/docs/content/docs/fr/mcp/index.mdx
index 830f0a6151..97fb04ffab 100644
--- a/apps/docs/content/docs/fr/mcp/index.mdx
+++ b/apps/docs/content/docs/fr/mcp/index.mdx
@@ -1,8 +1,11 @@
---
-title: MCP (Model Context Protocol)
+title: Utiliser les outils MCP
+description: Connectez des outils et services externes en utilisant le Model
+ Context Protocol
---
import { Image } from '@/components/ui/image'
+import { Video } from '@/components/ui/video'
import { Callout } from 'fumadocs-ui/components/callout'
Le Model Context Protocol ([MCP](https://modelcontextprotocol.com/)) vous permet de connecter des outils et services externes en utilisant un protocole standardisé, vous permettant d'intégrer des API et des services directement dans vos flux de travail. Avec MCP, vous pouvez étendre les capacités de Sim en ajoutant des intégrations personnalisées qui fonctionnent parfaitement avec vos agents et flux de travail.
@@ -20,14 +23,8 @@ MCP est une norme ouverte qui permet aux assistants IA de se connecter de maniè
Les serveurs MCP fournissent des collections d'outils que vos agents peuvent utiliser. Configurez-les dans les paramètres de l'espace de travail :
-
-
+
+
1. Accédez aux paramètres de votre espace de travail
@@ -40,14 +37,18 @@ Les serveurs MCP fournissent des collections d'outils que vos agents peuvent uti
Vous pouvez également configurer les serveurs MCP directement depuis la barre d'outils d'un bloc Agent pour une configuration rapide.
-## Utilisation des outils MCP dans les agents
+### Actualiser les outils
+
+Cliquez sur **Actualiser** sur un serveur pour récupérer les derniers schémas d'outils et mettre à jour automatiquement tous les blocs d'agent utilisant ces outils avec les nouvelles définitions de paramètres.
+
+## Utiliser les outils MCP dans les agents
-Une fois les serveurs MCP configurés, leurs outils deviennent disponibles dans vos blocs d'agents :
+Une fois les serveurs MCP configurés, leurs outils deviennent disponibles dans vos blocs d'agent :
+
+## Instructions d'utilisation
+
+Intégrez-vous avec Jira Service Management pour la gestion des services informatiques. Créez et gérez des demandes de service, traitez les clients et les organisations, suivez les SLA et gérez les files d'attente.
+
+## Outils
+
+### `jsm_get_service_desks`
+
+Obtenir tous les centres de services depuis Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `serviceDesks` | json | Tableau des centres de services |
+| `total` | number | Nombre total de centres de services |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_get_request_types`
+
+Obtenir les types de demandes pour un centre de services dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `serviceDeskId` | string | Oui | ID du centre de services pour lequel obtenir les types de demandes |
+| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `requestTypes` | json | Tableau des types de demande |
+| `total` | number | Nombre total de types de demande |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_create_request`
+
+Créer une nouvelle demande de service dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `serviceDeskId` | string | Oui | ID du Service Desk dans lequel créer la demande |
+| `requestTypeId` | string | Oui | ID du type de demande pour la nouvelle demande |
+| `summary` | string | Oui | Résumé/titre de la demande de service |
+| `description` | string | Non | Description de la demande de service |
+| `raiseOnBehalfOf` | string | Non | ID de compte du client pour lequel créer la demande |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueId` | string | ID du ticket de demande créé |
+| `issueKey` | string | Clé du ticket de demande créé \(par exemple, SD-123\) |
+| `requestTypeId` | string | ID du type de demande |
+| `serviceDeskId` | string | ID du Service Desk |
+| `success` | boolean | Indique si la demande a été créée avec succès |
+| `url` | string | URL vers la demande créée |
+
+### `jsm_get_request`
+
+Obtenir une seule demande de service depuis Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+
+### `jsm_get_requests`
+
+Obtenir plusieurs demandes de service depuis Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `serviceDeskId` | string | Non | Filtrer par ID de service desk |
+| `requestOwnership` | string | Non | Filtrer par propriété : OWNED_REQUESTS, PARTICIPATED_REQUESTS, ORGANIZATION, ALL_REQUESTS |
+| `requestStatus` | string | Non | Filtrer par statut : OPEN, CLOSED, ALL |
+| `searchTerm` | string | Non | Terme de recherche pour filtrer les demandes |
+| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `requests` | json | Tableau des demandes de service |
+| `total` | number | Nombre total de demandes |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_add_comment`
+
+Ajouter un commentaire (public ou interne) à une demande de service dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
+| `body` | string | Oui | Texte du corps du commentaire |
+| `isPublic` | boolean | Oui | Indique si le commentaire est public \(visible par le client\) ou interne |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `commentId` | string | ID du commentaire créé |
+| `body` | string | Texte du corps du commentaire |
+| `isPublic` | boolean | Indique si le commentaire est public |
+| `success` | boolean | Indique si le commentaire a été ajouté avec succès |
+
+### `jsm_get_comments`
+
+Obtenir les commentaires d'une demande de service dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
+| `isPublic` | boolean | Non | Filtrer uniquement les commentaires publics |
+| `internal` | boolean | Non | Filtrer uniquement les commentaires internes |
+| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `comments` | json | Tableau des commentaires |
+| `total` | number | Nombre total de commentaires |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_get_customers`
+
+Obtenir les clients d'un service desk dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Jira Cloud de l'instance |
+| `serviceDeskId` | string | Oui | ID du service desk pour lequel obtenir les clients |
+| `query` | string | Non | Requête de recherche pour filtrer les clients |
+| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `customers` | json | Tableau des clients |
+| `total` | number | Nombre total de clients |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_add_customer`
+
+Ajouter des clients à un service desk dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `serviceDeskId` | string | Oui | ID du Service Desk auquel ajouter des clients |
+| `emails` | string | Oui | Adresses e-mail séparées par des virgules à ajouter comme clients |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `serviceDeskId` | string | ID du Service Desk |
+| `success` | boolean | Indique si les clients ont été ajoutés avec succès |
+
+### `jsm_get_organizations`
+
+Obtenir les organisations d'un Service Desk dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `serviceDeskId` | string | Oui | ID du Service Desk pour lequel obtenir les organisations |
+| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `organizations` | json | Tableau des organisations |
+| `total` | number | Nombre total d'organisations |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_create_organization`
+
+Créer une nouvelle organisation dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `name` | string | Oui | Nom de l'organisation à créer |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `organizationId` | string | ID de l'organisation créée |
+| `name` | string | Nom de l'organisation créée |
+| `success` | boolean | Indique si l'opération a réussi |
+
+### `jsm_add_organization_to_service_desk`
+
+Ajouter une organisation à un service desk dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `serviceDeskId` | string | Oui | ID du service desk auquel ajouter l'organisation |
+| `organizationId` | string | Oui | ID de l'organisation à ajouter au service desk |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `serviceDeskId` | string | ID du service desk |
+| `organizationId` | string | ID de l'organisation ajoutée |
+| `success` | boolean | Indique si l'opération a réussi |
+
+### `jsm_get_queues`
+
+Obtenir les files d'attente pour un service desk dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira (par ex., votreentreprise.atlassian.net) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `serviceDeskId` | string | Oui | ID du service desk pour lequel obtenir les files d'attente |
+| `includeCount` | boolean | Non | Inclure le nombre de tickets pour chaque file d'attente |
+| `start` | number | Non | Index de départ pour la pagination (par défaut : 0) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner (par défaut : 50) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `queues` | json | Tableau des files d'attente |
+| `total` | number | Nombre total de files d'attente |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_get_sla`
+
+Obtenir les informations SLA pour une demande de service dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira (par ex., votreentreprise.atlassian.net) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par ex., SD-123) |
+| `start` | number | Non | Index de départ pour la pagination (par défaut : 0) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner (par défaut : 50) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `slas` | json | Tableau des informations SLA |
+| `total` | number | Nombre total de SLA |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_get_transitions`
+
+Obtenir les transitions disponibles pour une demande de service dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira (par exemple, votreentreprise.atlassian.net) |
+| `cloudId` | string | Non | ID Jira Cloud de l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par exemple, SD-123) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `transitions` | json | Tableau des transitions disponibles |
+
+### `jsm_transition_request`
+
+Faire passer une demande de service à un nouveau statut dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira (par exemple, votreentreprise.atlassian.net) |
+| `cloudId` | string | Non | ID Jira Cloud de l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par exemple, SD-123) |
+| `transitionId` | string | Oui | ID de la transition à appliquer |
+| `comment` | string | Non | Commentaire optionnel à ajouter lors de la transition |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `transitionId` | string | ID de la transition appliquée |
+| `success` | boolean | Indique si la transition a réussi |
+
+### `jsm_get_participants`
+
+Obtenir les participants d'une demande dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira de l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
+| `start` | number | Non | Index de départ pour la pagination \(par défaut : 0\) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner \(par défaut : 50\) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `participants` | json | Tableau des participants |
+| `total` | number | Nombre total de participants |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_add_participants`
+
+Ajouter des participants à une demande dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira \(par exemple, votreentreprise.atlassian.net\) |
+| `cloudId` | string | Non | ID Cloud Jira de l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket \(par exemple, SD-123\) |
+| `accountIds` | string | Oui | ID de comptes séparés par des virgules à ajouter comme participants |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `participants` | json | Tableau des participants ajoutés |
+| `success` | boolean | Indique si l'opération a réussi |
+
+### `jsm_get_approvals`
+
+Obtenir les approbations pour une demande dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira (par exemple, votreentreprise.atlassian.net) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par exemple, SD-123) |
+| `start` | number | Non | Index de départ pour la pagination (par défaut : 0) |
+| `limit` | number | Non | Nombre maximum de résultats à retourner (par défaut : 50) |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `approvals` | json | Tableau des approbations |
+| `total` | number | Nombre total d'approbations |
+| `isLastPage` | boolean | Indique s'il s'agit de la dernière page |
+
+### `jsm_answer_approval`
+
+Approuver ou refuser une demande d'approbation dans Jira Service Management
+
+#### Entrée
+
+| Paramètre | Type | Requis | Description |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | Oui | Votre domaine Jira (par exemple, votreentreprise.atlassian.net) |
+| `cloudId` | string | Non | ID Cloud Jira pour l'instance |
+| `issueIdOrKey` | string | Oui | ID ou clé du ticket (par exemple, SD-123) |
+| `approvalId` | string | Oui | ID de l'approbation à traiter |
+| `decision` | string | Oui | Décision : "approve" ou "decline" |
+
+#### Sortie
+
+| Paramètre | Type | Description |
+| --------- | ---- | ----------- |
+| `ts` | string | Horodatage de l'opération |
+| `issueIdOrKey` | string | ID ou clé du ticket |
+| `approvalId` | string | ID d'approbation |
+| `decision` | string | Décision prise \(approuver/refuser\) |
+| `success` | boolean | Indique si l'opération a réussi |
+
+## Remarques
+
+- Catégorie : `tools`
+- Type : `jira_service_management`
diff --git a/apps/docs/content/docs/ja/mcp/deploy-workflows.mdx b/apps/docs/content/docs/ja/mcp/deploy-workflows.mdx
new file mode 100644
index 0000000000..e253982a33
--- /dev/null
+++ b/apps/docs/content/docs/ja/mcp/deploy-workflows.mdx
@@ -0,0 +1,107 @@
+---
+title: ワークフローをMCPとしてデプロイ
+description: 外部のAIアシスタントやアプリケーション向けに、ワークフローをMCPツールとして公開
+---
+
+import { Video } from '@/components/ui/video'
+import { Callout } from 'fumadocs-ui/components/callout'
+
+ワークフローをMCPツールとしてデプロイすることで、Claude Desktop、Cursor、その他のMCP互換クライアントなどの外部AIアシスタントからアクセス可能になります。これにより、ワークフローがどこからでも呼び出せる呼び出し可能なツールに変わります。
+
+## MCPサーバーの作成と管理
+
+MCPサーバーは、ワークフローツールをまとめてグループ化します。ワークスペース設定で作成と管理を行います。
+
+
+
+
+
+1. **設定 → MCPサーバー**に移動
+2. **サーバーを作成**をクリック
+3. 名前と説明(任意)を入力
+4. MCPクライアントで使用するためにサーバーURLをコピー
+5. サーバーに追加されたすべてのツールを表示・管理
+
+## ワークフローをツールとして追加
+
+ワークフローがデプロイされたら、MCPツールとして公開できます。
+
+
+
+
+
+1. デプロイ済みのワークフローを開く
+2. **デプロイ**をクリックし、**MCP**タブに移動
+3. ツール名と説明を設定
+4. 各パラメータの説明を追加(AIが入力を理解するのに役立ちます)
+5. 追加先のMCPサーバーを選択
+
+
+ワークフローをMCPツールとして追加する前に、デプロイしておく必要があります。
+
+
+## ツールの設定
+
+### ツール名
+小文字、数字、アンダースコアを使用します。名前は説明的で、MCPの命名規則に従う必要があります(例: `search_documents`、`send_email`)。
+
+### 説明
+ツールが何をするのかを明確に説明します。これにより、AIアシスタントがツールをいつ使用すべきかを理解できます。
+
+### パラメータ
+ワークフローの入力形式フィールドがツールパラメータになります。AIアシスタントが正しい値を提供できるよう、各パラメータに説明を追加してください。
+
+## MCPクライアントの接続
+
+設定から取得したサーバーURLを使用して外部アプリケーションを接続します:
+
+### Claude Desktop
+Claude Desktopの設定ファイル(`~/Library/Application Support/Claude/claude_desktop_config.json`)に追加してください:
+
+```json
+{
+ "mcpServers": {
+ "my-sim-workflows": {
+ "command": "npx",
+ "args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
+ }
+ }
+}
+```
+
+### Cursor
+同じmcp-remoteパターンを使用して、CursorのMCP設定にサーバーURLを追加してください。
+
+
+mcp-remoteまたは他のHTTPベースのMCPトランスポートを使用する際は、認証アクセスのためにAPIキーヘッダー(`X-API-Key`)を含めてください。
+
+
+## サーバー管理
+
+**設定 → MCPサーバー**のサーバー詳細ビューから、以下の操作が可能です:
+
+- **ツールを表示**: サーバーに追加されたすべてのワークフローを確認
+- **URLをコピー**: MCPクライアント用のサーバーURLを取得
+- **ワークフローを追加**: デプロイ済みワークフローをツールとして追加
+- **ツールを削除**: サーバーからワークフローを削除
+- **サーバーを削除**: サーバー全体とすべてのツールを削除
+
+## 仕組み
+
+MCPクライアントがツールを呼び出すと:
+
+1. MCPサーバーURLでリクエストを受信
+2. Simがリクエストを検証し、パラメータをワークフロー入力にマッピング
+3. 提供された入力でデプロイ済みワークフローを実行
+4. 結果をMCPクライアントに返却
+
+ワークフローはAPI呼び出しと同じデプロイバージョンを使用して実行されるため、一貫した動作が保証されます。
+
+## 必要な権限
+
+| アクション | 必要な権限 |
+|--------|-------------------|
+| MCPサーバーを作成 | **管理者** |
+| サーバーにワークフローを追加 | **書き込み**または**管理者** |
+| MCPサーバーを表示 | **読み取り**、**書き込み**、または**管理者** |
+| MCPサーバーを削除 | **管理者** |
diff --git a/apps/docs/content/docs/ja/mcp/index.mdx b/apps/docs/content/docs/ja/mcp/index.mdx
index 3e3a3793e3..48907a92f5 100644
--- a/apps/docs/content/docs/ja/mcp/index.mdx
+++ b/apps/docs/content/docs/ja/mcp/index.mdx
@@ -1,8 +1,10 @@
---
-title: MCP(モデルコンテキストプロトコル)
+title: MCPツールの使用
+description: Model Context Protocolを使用して外部ツールとサービスを接続
---
import { Image } from '@/components/ui/image'
+import { Video } from '@/components/ui/video'
import { Callout } from 'fumadocs-ui/components/callout'
モデルコンテキストプロトコル([MCP](https://modelcontextprotocol.com/))を使用すると、標準化されたプロトコルを使用して外部ツールやサービスを接続し、APIやサービスをワークフローに直接統合することができます。MCPを使用することで、エージェントやワークフローとシームレスに連携するカスタム統合機能を追加して、Simの機能を拡張できます。
@@ -20,14 +22,8 @@ MCPは、AIアシスタントが外部データソースやツールに安全に
MCPサーバーはエージェントが使用できるツールのコレクションを提供します。ワークスペース設定で構成してください:
-
-
+
+
1. ワークスペース設定に移動します
@@ -40,9 +36,13 @@ MCPサーバーはエージェントが使用できるツールのコレクシ
エージェントブロックのツールバーから直接MCPサーバーを構成することもできます(クイックセットアップ)。
+### ツールの更新
+
+サーバーの**更新**をクリックすると、最新のツールスキーマを取得し、それらのツールを使用しているエージェントブロックを新しいパラメータ定義で自動的に更新します。
+
## エージェントでのMCPツールの使用
-MCPサーバーが構成されると、そのツールはエージェントブロック内で利用可能になります:
+MCPサーバーが設定されると、そのツールがエージェントブロック内で利用可能になります:
-1. **エージェント**ブロックを開きます
+1. **エージェント**ブロックを開く
2. **ツール**セクションで、利用可能なMCPツールが表示されます
-3. エージェントに使用させたいツールを選択します
-4. これでエージェントは実行中にこれらのツールにアクセスできるようになります
+3. エージェントに使用させたいツールを選択
+4. エージェントは実行中にこれらのツールにアクセスできるようになります
## スタンドアロンMCPツールブロック
-より細かい制御のために、特定のMCPツールを実行するための専用MCPツールブロックを使用できます:
+より細かい制御が必要な場合は、専用のMCPツールブロックを使用して特定のMCPツールを実行できます:
-MCPツールブロックでは以下のことが可能です:
-- 構成済みのMCPツールを直接実行する
+MCPツールブロックでは次のことができます:
+- 設定済みのMCPツールを直接実行
- ツールに特定のパラメータを渡す
-- ツールの出力を後続のワークフローステップで使用する
+- ツールの出力を後続のワークフローステップで使用
- 複数のMCPツールを連鎖させる
### MCPツールとエージェントの使い分け
-**エージェントとMCPツールを使用する場合:**
-- AIにどのツールを使用するか決定させたい場合
-- ツールをいつどのように使用するかについて複雑な推論が必要な場合
-- ツールと自然言語でのやり取りが必要な場合
+**MCPツールを使用したエージェントを使用する場合:**
+- AIにどのツールを使用するか決定させたい
+- ツールをいつどのように使用するかについて複雑な推論が必要
+- ツールとの自然言語による対話が必要
**MCPツールブロックを使用する場合:**
- 決定論的なツール実行が必要な場合
@@ -97,34 +97,34 @@ MCP機能には特定のワークスペース権限が必要です:
| アクション | 必要な権限 |
|--------|-------------------|
-| 設定でMCPサーバーを構成する | **管理者** |
-| エージェントでMCPツールを使用する | **書き込み** または **管理者** |
-| 利用可能なMCPツールを表示する | **読み取り**、**書き込み**、または **管理者** |
-| MCPツールブロックを実行する | **書き込み** または **管理者** |
+| 設定でMCPサーバーを構成 | **管理者** |
+| エージェントでMCPツールを使用 | **書き込み**または**管理者** |
+| 利用可能なMCPツールを表示 | **読み取り**、**書き込み**、または**管理者** |
+| MCPツールブロックを実行 | **書き込み**または**管理者** |
-## 一般的なユースケース
+## 一般的な使用例
### データベース統合
-ワークフロー内でデータのクエリ、挿入、更新を行うためにデータベースに接続します。
+データベースに接続して、ワークフロー内でデータのクエリ、挿入、更新を行います。
### API統合
-組み込みのSim統合がない外部APIやWebサービスにアクセスします。
+Simに組み込まれていない外部APIやWebサービスにアクセスします。
### ファイルシステムアクセス
-ローカルまたはリモートファイルシステム上のファイルの読み取り、書き込み、操作を行います。
+ローカルまたはリモートのファイルシステム上のファイルの読み取り、書き込み、操作を行います。
### カスタムビジネスロジック
-組織のニーズに特化したカスタムスクリプトやツールを実行します。
+組織固有のニーズに合わせたカスタムスクリプトやツールを実行します。
### リアルタイムデータアクセス
ワークフロー実行中に外部システムからライブデータを取得します。
## セキュリティに関する考慮事項
-- MCPサーバーは構成したユーザーの権限で実行されます
+- MCPサーバーは、それを構成したユーザーの権限で実行されます
- インストール前に必ずMCPサーバーのソースを確認してください
- 機密性の高い構成データには環境変数を使用してください
-- エージェントにアクセス権を付与する前にMCPサーバーの機能を確認してください
+- エージェントにアクセスを許可する前にMCPサーバーの機能を確認してください
## トラブルシューティング
@@ -139,6 +139,6 @@ MCP機能には特定のワークスペース権限が必要です:
- 必要な認証が構成されていることを確認してください
### 権限エラー
-- ワークスペースの権限レベルを確認する
-- MCPサーバーが追加認証を必要としているか確認する
-- サーバーがワークスペース用に適切に構成されているか確認する
\ No newline at end of file
+- ワークスペースの権限レベルを確認してください
+- MCPサーバーが追加の認証を必要としているか確認してください
+- サーバーがワークスペースに対して適切に設定されているか確認してください
\ No newline at end of file
diff --git a/apps/docs/content/docs/ja/tools/jira_service_management.mdx b/apps/docs/content/docs/ja/tools/jira_service_management.mdx
new file mode 100644
index 0000000000..f34a9af869
--- /dev/null
+++ b/apps/docs/content/docs/ja/tools/jira_service_management.mdx
@@ -0,0 +1,486 @@
+---
+title: Jira Service Management
+description: Jira Service Managementと連携する
+---
+
+import { BlockInfoCard } from "@/components/ui/block-info-card"
+
+
+
+## 使用方法
+
+ITサービス管理のためにJira Service Managementと統合します。サービスリクエストの作成と管理、顧客と組織の処理、SLAの追跡、キューの管理を行います。
+
+## ツール
+
+### `jsm_get_service_desks`
+
+Jira Service Managementからすべてのサービスデスクを取得する
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `serviceDesks` | json | サービスデスクの配列 |
+| `total` | number | サービスデスクの総数 |
+| `isLastPage` | boolean | これが最後のページかどうか |
+
+### `jsm_get_request_types`
+
+Jira Service Managementのサービスデスクのリクエストタイプを取得する
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `serviceDeskId` | string | はい | リクエストタイプを取得するサービスデスクID |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `requestTypes` | json | リクエストタイプの配列 |
+| `total` | number | リクエストタイプの総数 |
+| `isLastPage` | boolean | これが最後のページかどうか |
+
+### `jsm_create_request`
+
+Jira Service Managementで新しいサービスリクエストを作成
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `serviceDeskId` | string | はい | リクエストを作成するサービスデスクID |
+| `requestTypeId` | string | はい | 新しいリクエストのリクエストタイプID |
+| `summary` | string | はい | サービスリクエストの概要/タイトル |
+| `description` | string | いいえ | サービスリクエストの説明 |
+| `raiseOnBehalfOf` | string | いいえ | 代理でリクエストを作成する顧客のアカウントID |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueId` | string | 作成されたリクエストの課題ID |
+| `issueKey` | string | 作成されたリクエストの課題キー(例:SD-123) |
+| `requestTypeId` | string | リクエストタイプID |
+| `serviceDeskId` | string | サービスデスクID |
+| `success` | boolean | リクエストが正常に作成されたかどうか |
+| `url` | string | 作成されたリクエストのURL |
+
+### `jsm_get_request`
+
+Jira Service Managementから単一のサービスリクエストを取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+
+### `jsm_get_requests`
+
+Jira Service Managementから複数のサービスリクエストを取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `serviceDeskId` | string | いいえ | サービスデスクIDでフィルタ |
+| `requestOwnership` | string | いいえ | 所有権でフィルタ:OWNED_REQUESTS、PARTICIPATED_REQUESTS、ORGANIZATION、ALL_REQUESTS |
+| `requestStatus` | string | いいえ | ステータスでフィルタ:OPEN、CLOSED、ALL |
+| `searchTerm` | string | いいえ | リクエストをフィルタする検索語 |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `requests` | json | サービスリクエストの配列 |
+| `total` | number | リクエストの総数 |
+| `isLastPage` | boolean | これが最後のページかどうか |
+
+### `jsm_add_comment`
+
+Jira Service Managementのサービスリクエストにコメント(公開または内部)を追加する
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
+| `body` | string | はい | コメント本文 |
+| `isPublic` | boolean | はい | コメントが公開(顧客に表示)か内部かを指定 |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `commentId` | string | 作成されたコメントID |
+| `body` | string | コメント本文 |
+| `isPublic` | boolean | コメントが公開かどうか |
+| `success` | boolean | コメントが正常に追加されたかどうか |
+
+### `jsm_get_comments`
+
+Jira Service Managementのサービスリクエストのコメントを取得する
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
+| `isPublic` | boolean | いいえ | 公開コメントのみにフィルタ |
+| `internal` | boolean | いいえ | 内部コメントのみにフィルタ |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `comments` | json | コメントの配列 |
+| `total` | number | コメントの総数 |
+| `isLastPage` | boolean | 最後のページかどうか |
+
+### `jsm_get_customers`
+
+Jira Service Managementのサービスデスクの顧客を取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `serviceDeskId` | string | はい | 顧客を取得するサービスデスクID |
+| `query` | string | いいえ | 顧客をフィルタリングする検索クエリ |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト: 0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト: 50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `customers` | json | 顧客の配列 |
+| `total` | number | 顧客の総数 |
+| `isLastPage` | boolean | 最後のページかどうか |
+
+### `jsm_add_customer`
+
+Jira Service Managementのサービスデスクに顧客を追加
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `serviceDeskId` | string | はい | 顧客を追加するサービスデスクID |
+| `emails` | string | はい | 顧客として追加するメールアドレス(カンマ区切り) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `serviceDeskId` | string | サービスデスクID |
+| `success` | boolean | 顧客が正常に追加されたかどうか |
+
+### `jsm_get_organizations`
+
+Jira Service Managementのサービスデスクの組織を取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `serviceDeskId` | string | はい | 組織を取得するサービスデスクID |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `organizations` | json | 組織の配列 |
+| `total` | number | 組織の総数 |
+| `isLastPage` | boolean | これが最後のページかどうか |
+
+### `jsm_create_organization`
+
+Jira Service Managementで新しい組織を作成する
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `name` | string | はい | 作成する組織の名前 |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `organizationId` | string | 作成された組織のID |
+| `name` | string | 作成された組織の名前 |
+| `success` | boolean | 操作が成功したかどうか |
+
+### `jsm_add_organization_to_service_desk`
+
+Jira Service Managementのサービスデスクに組織を追加する
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `serviceDeskId` | string | はい | 組織を追加するサービスデスクID |
+| `organizationId` | string | はい | サービスデスクに追加する組織ID |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `serviceDeskId` | string | サービスデスクID |
+| `organizationId` | string | 追加された組織ID |
+| `success` | boolean | 操作が成功したかどうか |
+
+### `jsm_get_queues`
+
+Jira Service Managementでサービスデスクのキューを取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `serviceDeskId` | string | はい | キューを取得するサービスデスクID |
+| `includeCount` | boolean | いいえ | 各キューの課題数を含める |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `queues` | json | キューの配列 |
+| `total` | number | キューの総数 |
+| `isLastPage` | boolean | これが最後のページかどうか |
+
+### `jsm_get_sla`
+
+Jira Service ManagementでサービスリクエストのSLA情報を取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `slas` | json | SLA情報の配列 |
+| `total` | number | SLAの総数 |
+| `isLastPage` | boolean | 最後のページかどうか |
+
+### `jsm_get_transitions`
+
+Jira Service Managementのサービスリクエストで利用可能なトランジションを取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例: SD-123) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `transitions` | json | 利用可能なトランジションの配列 |
+
+### `jsm_transition_request`
+
+Jira Service Managementでサービスリクエストを新しいステータスにトランジション
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例: SD-123) |
+| `transitionId` | string | はい | 適用するトランジションID |
+| `comment` | string | いいえ | トランジション時に追加するオプションのコメント |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `transitionId` | string | 適用されたトランジションID |
+| `success` | boolean | トランジションが成功したかどうか |
+
+### `jsm_get_participants`
+
+Jira Service Managementのリクエストの参加者を取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例: SD-123) |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト: 0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト: 50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `participants` | json | 参加者の配列 |
+| `total` | number | 参加者の総数 |
+| `isLastPage` | boolean | これが最後のページかどうか |
+
+### `jsm_add_participants`
+
+Jira Service Managementのリクエストに参加者を追加
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例: yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例: SD-123) |
+| `accountIds` | string | はい | 参加者として追加するアカウントIDのカンマ区切りリスト |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `participants` | json | 追加された参加者の配列 |
+| `success` | boolean | 操作が成功したかどうか |
+
+### `jsm_get_approvals`
+
+Jira Service Managementでリクエストの承認を取得
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
+| `start` | number | いいえ | ページネーションの開始インデックス(デフォルト:0) |
+| `limit` | number | いいえ | 返す最大結果数(デフォルト:50) |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `approvals` | json | 承認の配列 |
+| `total` | number | 承認の総数 |
+| `isLastPage` | boolean | これが最後のページかどうか |
+
+### `jsm_answer_approval`
+
+Jira Service Managementで承認リクエストを承認または却下
+
+#### 入力
+
+| パラメータ | 型 | 必須 | 説明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | はい | Jiraドメイン(例:yourcompany.atlassian.net) |
+| `cloudId` | string | いいえ | インスタンスのJira Cloud ID |
+| `issueIdOrKey` | string | はい | 課題IDまたはキー(例:SD-123) |
+| `approvalId` | string | はい | 回答する承認ID |
+| `decision` | string | はい | 決定:「approve」または「decline」 |
+
+#### 出力
+
+| パラメータ | 型 | 説明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作のタイムスタンプ |
+| `issueIdOrKey` | string | 課題IDまたはキー |
+| `approvalId` | string | 承認ID |
+| `decision` | string | 決定内容(承認/却下) |
+| `success` | boolean | 操作が成功したかどうか |
+
+## 注意事項
+
+- カテゴリ: `tools`
+- タイプ: `jira_service_management`
diff --git a/apps/docs/content/docs/zh/mcp/deploy-workflows.mdx b/apps/docs/content/docs/zh/mcp/deploy-workflows.mdx
new file mode 100644
index 0000000000..982883053e
--- /dev/null
+++ b/apps/docs/content/docs/zh/mcp/deploy-workflows.mdx
@@ -0,0 +1,107 @@
+---
+title: 将工作流部署为 MCP
+description: 将您的工作流公开为 MCP 工具,供外部 AI 助手和应用程序使用
+---
+
+import { Video } from '@/components/ui/video'
+import { Callout } from 'fumadocs-ui/components/callout'
+
+将您的工作流部署为 MCP 工具,使其可供外部 AI 助手(如 Claude Desktop、Cursor 以及其他兼容 MCP 的客户端)访问。这会让您的工作流变成可随时调用的工具。
+
+## 创建和管理 MCP 服务器
+
+MCP 服务器用于将您的工作流工具进行分组。您可以在工作区设置中创建和管理这些服务器:
+
+
+
+
+
+1. 进入 **设置 → MCP 服务器**
+2. 点击 **创建服务器**
+3. 输入名称和可选描述
+4. 复制服务器 URL 以在您的 MCP 客户端中使用
+5. 查看并管理已添加到服务器的所有工具
+
+## 添加工作流为工具
+
+当您的工作流部署完成后,可以将其公开为 MCP 工具:
+
+
+
+
+
+1. 打开已部署的工作流
+2. 点击 **部署** 并进入 **MCP** 标签页
+3. 配置工具名称和描述
+4. 为每个参数添加描述(帮助 AI 理解输入)
+5. 选择要添加到的 MCP 服务器
+
+
+工作流必须先部署,才能添加为 MCP 工具。
+
+
+## 工具配置
+
+### 工具名称
+请使用小写字母、数字和下划线。名称应具有描述性,并遵循 MCP 命名规范(如 `search_documents`、`send_email`)。
+
+### 描述
+请清晰描述该工具的功能。这有助于 AI 助手理解何时使用该工具。
+
+### 参数
+您的工作流输入格式字段会变成工具参数。为每个参数添加描述,有助于 AI 助手提供正确的值。
+
+## 连接 MCP 客户端
+
+使用设置中的服务器 URL 连接外部应用程序:
+
+### Claude Desktop
+将以下内容添加到您的 Claude Desktop 配置中(`~/Library/Application Support/Claude/claude_desktop_config.json`):
+
+```json
+{
+ "mcpServers": {
+ "my-sim-workflows": {
+ "command": "npx",
+ "args": ["-y", "mcp-remote", "YOUR_SERVER_URL"]
+ }
+ }
+}
+```
+
+### Cursor
+在 Cursor 的 MCP 设置中,使用相同的 mcp-remote 格式添加服务器 URL。
+
+
+使用 mcp-remote 或其他基于 HTTP 的 MCP 传输方式时,请包含 API key header(`X-API-Key`)以进行身份验证访问。
+
+
+## 服务器管理
+
+在 **设置 → MCP 服务器** 的服务器详情视图中,您可以:
+
+- **查看工具**:查看添加到服务器的所有工作流
+- **复制 URL**:获取 MCP 客户端的服务器 URL
+- **添加工作流**:将更多已部署的工作流添加为工具
+- **移除工具**:从服务器中移除工作流
+- **删除服务器**:移除整个服务器及其所有工具
+
+## 工作原理
+
+当 MCP 客户端调用您的工具时:
+
+1. 请求会发送到您的 MCP 服务器 URL
+2. Sim 验证请求并将参数映射到工作流输入
+3. 已部署的工作流会使用提供的输入执行
+4. 结果返回给 MCP 客户端
+
+工作流执行时使用与 API 调用相同的部署版本,确保行为一致。
+
+## 权限要求
+
+| 操作 | 所需权限 |
+|--------|-------------------|
+| 创建 MCP 服务器 | **Admin** |
+| 向服务器添加工作流 | **Write** 或 **Admin** |
+| 查看 MCP 服务器 | **Read**、**Write** 或 **Admin** |
+| 删除 MCP 服务器 | **Admin** |
diff --git a/apps/docs/content/docs/zh/mcp/index.mdx b/apps/docs/content/docs/zh/mcp/index.mdx
index 3ee0e4af1e..d8d4263bc0 100644
--- a/apps/docs/content/docs/zh/mcp/index.mdx
+++ b/apps/docs/content/docs/zh/mcp/index.mdx
@@ -1,8 +1,10 @@
---
-title: MCP(模型上下文协议)
+title: 使用 MCP 工具
+description: 通过 Model Context Protocol 连接外部工具和服务
---
import { Image } from '@/components/ui/image'
+import { Video } from '@/components/ui/video'
import { Callout } from 'fumadocs-ui/components/callout'
模型上下文协议([MCP](https://modelcontextprotocol.com/))允许您使用标准化协议连接外部工具和服务,从而将 API 和服务直接集成到您的工作流程中。通过 MCP,您可以通过添加自定义集成来扩展 Sim 的功能,使其与您的代理和工作流程无缝协作。
@@ -20,14 +22,8 @@ MCP 是一项开放标准,使 AI 助手能够安全地连接到外部数据源
MCP 服务器提供工具集合,供您的代理使用。您可以在工作区设置中进行配置:
-
-
+
+
1. 进入您的工作区设置
@@ -40,56 +36,60 @@ MCP 服务器提供工具集合,供您的代理使用。您可以在工作区
您还可以直接从代理模块的工具栏中配置 MCP 服务器,以便快速设置。
-## 在代理中使用 MCP 工具
+### 刷新工具
+
+点击服务器上的 **刷新**,即可获取最新的工具 schema,并自动用新的参数定义更新所有使用这些工具的 agent 模块。
+
+## 在 Agent 中使用 MCP 工具
-一旦配置了 MCP 服务器,其工具将在您的代理模块中可用:
+配置好 MCP 服务器后,其工具会在你的 agent 模块中可用:
-1. 打开一个 **代理** 模块
-2. 在 **工具** 部分,您将看到可用的 MCP 工具
-3. 选择您希望代理使用的工具
-4. 代理现在可以在执行过程中访问这些工具
+1. 打开一个 **Agent** 模块
+2. 在 **工具** 部分,你会看到可用的 MCP 工具
+3. 选择你希望 agent 使用的工具
+4. agent 在执行时即可访问这些工具
-## 独立的 MCP 工具模块
+## 独立 MCP 工具模块
-为了更精细的控制,您可以使用专用的 MCP 工具模块来执行特定的 MCP 工具:
+如需更细致的控制,可以使用专用的 MCP 工具模块来执行特定的 MCP 工具:
-MCP 工具模块允许您:
-- 直接执行任何已配置的 MCP 工具
+MCP 工具模块可以让你:
+- 直接执行任意已配置的 MCP 工具
- 向工具传递特定参数
-- 在后续工作流步骤中使用工具的输出
-- 将多个 MCP 工具串联在一起
+- 在后续工作流步骤中使用工具输出
+- 串联多个 MCP 工具
-### 何时使用 MCP 工具与代理
+### 何时使用 MCP 工具模块与 Agent
-**在以下情况下使用带有 MCP 工具的代理:**
-- 您希望 AI 决定使用哪些工具
-- 您需要复杂的推理来决定何时以及如何使用工具
-- 您希望与工具进行自然语言交互
+**当你需要以下场景时,使用 Agent 搭配 MCP 工具:**
+- 希望 AI 决定使用哪些工具
+- 需要复杂推理来判断何时及如何使用工具
+- 希望通过自然语言与工具交互
**在以下情况下使用 MCP 工具块:**
-- 您需要确定性的工具执行
-- 您希望使用已知参数执行特定工具
-- 您正在构建具有可预测步骤的结构化工作流
+- 你需要确定性的工具执行
+- 你想用已知参数执行特定工具
+- 你正在构建具有可预测步骤的结构化工作流
## 权限要求
@@ -97,48 +97,48 @@ MCP 功能需要特定的工作区权限:
| 操作 | 所需权限 |
|--------|-------------------|
-| 在设置中配置 MCP 服务器 | **管理员** |
-| 在代理中使用 MCP 工具 | **写入** 或 **管理员** |
-| 查看可用的 MCP 工具 | **读取**、**写入** 或 **管理员** |
-| 执行 MCP 工具块 | **写入** 或 **管理员** |
+| 在设置中配置 MCP 服务器 | **Admin** |
+| 在代理中使用 MCP 工具 | **Write** 或 **Admin** |
+| 查看可用的 MCP 工具 | **Read**、**Write** 或 **Admin** |
+| 执行 MCP 工具块 | **Write** 或 **Admin** |
-## 常见使用场景
+## 常见用例
### 数据库集成
-连接到数据库以在工作流中查询、插入或更新数据。
+在你的工作流中连接数据库以查询、插入或更新数据。
### API 集成
访问没有内置 Sim 集成的外部 API 和 Web 服务。
### 文件系统访问
-读取、写入和操作本地或远程文件系统上的文件。
+在本地或远程文件系统上读取、写入和操作文件。
### 自定义业务逻辑
-执行特定于您组织需求的自定义脚本或工具。
+执行针对你组织需求的自定义脚本或工具。
### 实时数据访问
在工作流执行期间从外部系统获取实时数据。
## 安全注意事项
-- MCP 服务器以配置它的用户权限运行
-- 安装前始终验证 MCP 服务器来源
-- 对于敏感的配置数据,请使用环境变量
-- 在授予代理访问权限之前,审查 MCP 服务器功能
+- MCP 服务器以配置它们的用户权限运行
+- 安装前务必验证 MCP 服务器来源
+- 对敏感配置信息使用环境变量
+- 在授予代理访问权限前,审查 MCP 服务器的功能
-## 故障排除
+## 故障排查
### MCP 服务器未显示
- 验证服务器配置是否正确
-- 检查您是否具有所需权限
+- 检查你是否拥有所需权限
- 确保 MCP 服务器正在运行且可访问
### 工具执行失败
- 验证工具参数格式是否正确
-- 检查 MCP 服务器日志中的错误消息
+- 检查 MCP 服务器日志中的错误信息
- 确保已配置所需的身份验证
### 权限错误
-- 确认您的工作区权限级别
-- 检查 MCP 服务器是否需要额外的身份验证
-- 验证服务器是否已为您的工作区正确配置
\ No newline at end of file
+- 确认你的工作区权限级别
+- 检查 MCP 服务器是否需要额外认证
+- 验证服务器是否已为你的工作区正确配置
\ No newline at end of file
diff --git a/apps/docs/content/docs/zh/tools/jira_service_management.mdx b/apps/docs/content/docs/zh/tools/jira_service_management.mdx
new file mode 100644
index 0000000000..dad3a2d1ec
--- /dev/null
+++ b/apps/docs/content/docs/zh/tools/jira_service_management.mdx
@@ -0,0 +1,486 @@
+---
+title: Jira Service Management
+description: 与 Jira Service Management 互动
+---
+
+import { BlockInfoCard } from "@/components/ui/block-info-card"
+
+
+
+## 使用说明
+
+集成 Jira Service Management 以进行 IT 服务管理。可创建和管理服务请求,处理客户和组织,跟踪 SLA,并管理队列。
+
+## 工具
+
+### `jsm_get_service_desks`
+
+获取 Jira Service Management 中的所有服务台
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 你的 Jira 域名(例如:yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `start` | number | 否 | 分页起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `serviceDesks` | json | 服务台数组 |
+| `total` | number | 服务台总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_get_request_types`
+
+获取 Jira Service Management 中某个服务台的请求类型
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 你的 Jira 域名(例如:yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `serviceDeskId` | string | 是 | 要获取请求类型的服务台 ID |
+| `start` | number | 否 | 分页起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `requestTypes` | json | 请求类型的数组 |
+| `total` | number | 请求类型的总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_create_request`
+
+在 Jira Service Management 中创建新的服务请求
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | ------ | ----------- |
+| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `serviceDeskId` | string | 是 | 要创建请求的服务台 ID |
+| `requestTypeId` | string | 是 | 新请求的请求类型 ID |
+| `summary` | string | 是 | 服务请求的摘要/标题 |
+| `description` | string | 否 | 服务请求的描述 |
+| `raiseOnBehalfOf` | string | 否 | 代表客户提交请求的账户 ID |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueId` | string | 创建的请求问题 ID |
+| `issueKey` | string | 创建的请求问题键(例如 SD-123) |
+| `requestTypeId` | string | 请求类型 ID |
+| `serviceDeskId` | string | 服务台 ID |
+| `success` | boolean | 请求是否创建成功 |
+| `url` | string | 创建的请求的 URL |
+
+### `jsm_get_request`
+
+从 Jira Service Management 获取单个服务请求
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net ) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或键(例如 SD-123 ) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+
+### `jsm_get_requests`
+
+从 Jira Service Management 获取多个服务请求
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net ) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `serviceDeskId` | string | 否 | 按服务台 ID 过滤 |
+| `requestOwnership` | string | 否 | 按所有权过滤: OWNED_REQUESTS 、 PARTICIPATED_REQUESTS 、 ORGANIZATION 、 ALL_REQUESTS |
+| `requestStatus` | string | 否 | 按状态过滤: OPEN 、 CLOSED 、 ALL |
+| `searchTerm` | string | 否 | 用于筛选请求的搜索词 |
+| `start` | number | 否 | 分页起始索引(默认值: 0 ) |
+| `limit` | number | 否 | 返回的最大结果数(默认值: 50 ) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `requests` | json | 服务请求数组 |
+| `total` | number | 请求总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_add_comment`
+
+在 Jira Service Management 中为服务请求添加评论(公开或内部)
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或关键字(例如 SD-123) |
+| `body` | string | 是 | 评论正文内容 |
+| `isPublic` | boolean | 是 | 评论是公开(对客户可见)还是内部 |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或关键字 |
+| `commentId` | string | 创建的评论 ID |
+| `body` | string | 评论正文内容 |
+| `isPublic` | boolean | 评论是否为公开 |
+| `success` | boolean | 评论是否添加成功 |
+
+### `jsm_get_comments`
+
+获取 Jira Service Management 中服务请求的评论
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或关键字(例如 SD-123) |
+| `isPublic` | boolean | 否 | 仅筛选公开评论 |
+| `internal` | boolean | 否 | 仅筛选内部评论 |
+| `start` | number | 否 | 分页起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或键值 |
+| `comments` | json | 评论数组 |
+| `total` | number | 评论总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_get_customers`
+
+获取 Jira Service Management 服务台的客户
+
+#### 输入
+
+| 参数 | 类型 | 是否必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 你的 Jira 域名(例如:yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `serviceDeskId` | string | 是 | 要获取客户的服务台 ID |
+| `query` | string | 否 | 用于筛选客户的搜索查询 |
+| `start` | number | 否 | 分页的起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `customers` | json | 客户数组 |
+| `total` | number | 客户总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_add_customer`
+
+向 Jira Service Management 服务台添加客户
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `serviceDeskId` | string | 是 | 要添加客户的服务台 ID |
+| `emails` | string | 是 | 以逗号分隔的要添加为客户的邮箱地址 |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `serviceDeskId` | string | 服务台 ID |
+| `success` | boolean | 是否成功添加了客户 |
+
+### `jsm_get_organizations`
+
+获取 Jira Service Management 服务台的组织
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `serviceDeskId` | string | 是 | 要获取组织的服务台 ID |
+| `start` | number | 否 | 分页起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `organizations` | json | 组织数组 |
+| `total` | number | 组织总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_create_organization`
+
+在 Jira Service Management 中创建一个新组织
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `name` | string | 是 | 要创建的组织名称 |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `organizationId` | string | 已创建组织的 ID |
+| `name` | string | 已创建组织的名称 |
+| `success` | boolean | 操作是否成功 |
+
+### `jsm_add_organization_to_service_desk`
+
+在 Jira Service Management 中将组织添加到服务台
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `serviceDeskId` | string | 是 | 要添加组织的服务台 ID |
+| `organizationId` | string | 是 | 要添加到服务台的组织 ID |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `serviceDeskId` | string | 服务台 ID |
+| `organizationId` | string | 已添加的组织 ID |
+| `success` | boolean | 操作是否成功 |
+
+### `jsm_get_queues`
+
+获取 Jira Service Management 中服务台的队列
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如:yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `serviceDeskId` | string | 是 | 要获取队列的服务台 ID |
+| `includeCount` | boolean | 否 | 是否包含每个队列的问题数量 |
+| `start` | number | 否 | 分页的起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `queues` | json | 队列数组 |
+| `total` | number | 队列总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_get_sla`
+
+获取 Jira Service Management 中服务请求的 SLA 信息
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如:yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或关键字(例如:SD-123) |
+| `start` | number | 否 | 分页的起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或键值 |
+| `slas` | json | SLA 信息数组 |
+| `total` | number | SLA 总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_get_transitions`
+
+获取 Jira Service Management 中服务请求的可用流转
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如:yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如:SD-123) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或键值 |
+| `transitions` | json | 可用流转的数组 |
+
+### `jsm_transition_request`
+
+将服务请求流转到 Jira Service Management 中的新状态
+
+#### 输入
+
+| 参数 | 类型 | 必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 您的 Jira 域名(例如:yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如:SD-123) |
+| `transitionId` | string | 是 | 要应用的流转 ID |
+| `comment` | string | 否 | 流转时可选的备注 |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或键值 |
+| `transitionId` | string | 已应用的转换 ID |
+| `success` | boolean | 转换是否成功 |
+
+### `jsm_get_participants`
+
+获取 Jira Service Management 请求的参与者
+
+#### 输入
+
+| 参数 | 类型 | 是否必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如 SD-123) |
+| `start` | number | 否 | 分页起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或键值 |
+| `participants` | json | 参与者数组 |
+| `total` | number | 参与者总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_add_participants`
+
+为 Jira Service Management 请求添加参与者
+
+#### 输入
+
+| 参数 | 类型 | 是否必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如 SD-123) |
+| `accountIds` | string | 是 | 以逗号分隔的要添加为参与者的账户 ID
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或键值 |
+| `participants` | json | 新增参与者数组 |
+| `success` | boolean | 操作是否成功 |
+
+### `jsm_get_approvals`
+
+在 Jira Service Management 中获取请求的审批信息
+
+#### 输入
+
+| 参数 | 类型 | 是否必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如 SD-123) |
+| `start` | number | 否 | 分页起始索引(默认值:0) |
+| `limit` | number | 否 | 返回的最大结果数(默认值:50) |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或键值 |
+| `approvals` | json | 审批数组 |
+| `total` | number | 审批总数 |
+| `isLastPage` | boolean | 是否为最后一页 |
+
+### `jsm_answer_approval`
+
+在 Jira Service Management 中批准或拒绝审批请求
+
+#### 输入
+
+| 参数 | 类型 | 是否必填 | 说明 |
+| --------- | ---- | -------- | ----------- |
+| `domain` | string | 是 | 你的 Jira 域名(例如 yourcompany.atlassian.net) |
+| `cloudId` | string | 否 | 实例的 Jira Cloud ID |
+| `issueIdOrKey` | string | 是 | 问题 ID 或键值(例如 SD-123) |
+| `approvalId` | string | 是 | 需要答复的审批 ID |
+| `decision` | string | 是 | 决策:“approve” 或 “decline” |
+
+#### 输出
+
+| 参数 | 类型 | 说明 |
+| --------- | ---- | ----------- |
+| `ts` | string | 操作的时间戳 |
+| `issueIdOrKey` | string | 问题 ID 或 key |
+| `approvalId` | string | 审批 ID |
+| `decision` | string | 做出的决定(approve/decline) |
+| `success` | boolean | 操作是否成功 |
+
+## 备注
+
+- 分类:`tools`
+- 类型:`jira_service_management`
diff --git a/apps/docs/i18n.lock b/apps/docs/i18n.lock
index cdf0bb63e8..ac46146208 100644
--- a/apps/docs/i18n.lock
+++ b/apps/docs/i18n.lock
@@ -4333,43 +4333,46 @@ checksums:
content/42: 824d6c95976aa25ee803b3591fb6f551
content/43: 864966620e19c8746d8180efdbdd9ae2
d73ad89134a95ad7e09fa05b910065aa:
- meta/title: f313501a9bef9974d92f1d62b42fdb93
- content/0: 0d95e6a3f94bfb96c6dc0d0784c08ea4
+ meta/title: 9c04618798e86e654d5079fe8de64db1
+ meta/description: 4da9477901d9df3c9450ba039d75dd37
+ content/0: 0d1f255ba4a6e466a883628166c021ea
content/1: 1f65550d1e29bd4153aff03620240d9a
content/2: d5b2535caa6733200d5bfe237593ce3c
content/3: b712a1e4dd21fd6b0e930199354adcc3
content/4: ca67b9905e9e11c157e61b0ab389538f
content/5: 6eee8c607e72b6c444d7b3ef07244f20
content/6: 747991e0e80e306dce1061ef7802db2a
- content/7: 704a7a653c2dc25f6695a6744ca751d3
+ content/7: 430153eacb29c66026cf71944df7be20
content/8: 5950966e19939b7a3a320d56ee4a674c
content/9: 159cf7a6d62e64b0c5db27e73b8c1ff5
- content/10: 5bcbd52289426d30d910b18bd3906752
- content/11: eea4f7ee41a1df57efbc383261657577
- content/12: 228fb76870c6a4f77ede7ee70b9e88ae
- content/13: 220fe4f7ae4350eb17a45ca7824f1588
- content/14: 8c8337d290379b6e1ed445e9be94a9d1
- content/15: 5d31ac96fab4fe61b0c50b04d3c5334d
- content/16: b6daf5b4a671686abe262dc86e1a8455
- content/17: 80c0384b94d64548d753f8029c2bba7f
- content/18: 606502329e2f62e12f33e39a640ceb10
- content/19: b57cfd7d01cf1ce7566d9adfd7629e93
- content/20: 3beb1b867645797faddd37a84bcb6277
- content/21: 47eb215a0fc230dc651b7bc05ab25ed0
- content/22: 175a21923c1df473224c54959ecbdb57
- content/23: 8305e779bb6f866f2e2998c374bc8af0
- content/24: a6b82eda019a997e4cb55f4461d0ae16
- content/25: ce8fdb26d2fcbd3f47f1032077622719
- content/26: 97855f8f10fd385774bc2dde42f96540
- content/27: 06cd80699da60d9bcce09ee32c0136fc
- content/28: ebaa0614d49146e17b04fb3be190209f
- content/29: 14d09c6d97ba08f6e7ea6be3ed854cad
- content/30: 90fb56f9810d8f8803f19be7907bee90
- content/31: fbf5d3ade971a3e039c715962db85ea9
- content/32: 623d40dc1cfdd82c4d805d6b02471c75
- content/33: 03c45c32e80d7d8875d339a0640f2f63
- content/34: 3d01b1e7080fee49ffb40b179873b676
- content/35: 81dcc5343377a51a32fe8d23fc172808
+ content/10: a723187777f9a848d4daa563e9dcbe17
+ content/11: b1c5f14e5290bcbbf5d590361ee7c053
+ content/12: 5bcbd52289426d30d910b18bd3906752
+ content/13: eea4f7ee41a1df57efbc383261657577
+ content/14: 228fb76870c6a4f77ede7ee70b9e88ae
+ content/15: 220fe4f7ae4350eb17a45ca7824f1588
+ content/16: 8c8337d290379b6e1ed445e9be94a9d1
+ content/17: 5d31ac96fab4fe61b0c50b04d3c5334d
+ content/18: b6daf5b4a671686abe262dc86e1a8455
+ content/19: 80c0384b94d64548d753f8029c2bba7f
+ content/20: 606502329e2f62e12f33e39a640ceb10
+ content/21: b57cfd7d01cf1ce7566d9adfd7629e93
+ content/22: 3beb1b867645797faddd37a84bcb6277
+ content/23: 47eb215a0fc230dc651b7bc05ab25ed0
+ content/24: 175a21923c1df473224c54959ecbdb57
+ content/25: 8305e779bb6f866f2e2998c374bc8af0
+ content/26: a6b82eda019a997e4cb55f4461d0ae16
+ content/27: ce8fdb26d2fcbd3f47f1032077622719
+ content/28: 97855f8f10fd385774bc2dde42f96540
+ content/29: 06cd80699da60d9bcce09ee32c0136fc
+ content/30: ebaa0614d49146e17b04fb3be190209f
+ content/31: 14d09c6d97ba08f6e7ea6be3ed854cad
+ content/32: 90fb56f9810d8f8803f19be7907bee90
+ content/33: fbf5d3ade971a3e039c715962db85ea9
+ content/34: 623d40dc1cfdd82c4d805d6b02471c75
+ content/35: 03c45c32e80d7d8875d339a0640f2f63
+ content/36: 3d01b1e7080fee49ffb40b179873b676
+ content/37: 81dcc5343377a51a32fe8d23fc172808
936c6450f0e3755fffa26ec3d3bd1b54:
meta/title: 2e89ff9f9632dacf671ce83787447240
content/0: 7e581dbf3e581d503ac94f7fb7938b1f
@@ -50000,3 +50003,166 @@ checksums:
content/34: 492b7c5af2dd4be062ee7af19778566a
content/35: b3f310d5ef115bea5a8b75bf25d7ea9a
content/36: 1305f85599a560b30c009091311a8dd0
+ f2beb64780f07155f616b02c172915c6:
+ meta/title: 41848121188bc58a7408e2aff659cd34
+ meta/description: 3d61074bd3acd724d95305b4d7d798bf
+ content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
+ content/1: 83ed540b35d2677d62ac9b033da7d19c
+ content/2: 821e6394b0a953e2b0842b04ae8f3105
+ content/3: 418dd7b6606b262ad61dfc2ef4cbbb4c
+ content/4: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
+ content/5: f45769b9935e467387f9d6b68603f25e
+ content/6: 8538751b324dd23fcc266ba0188d0130
+ content/7: 371d0e46b4bd2c23f559b8bc112f6955
+ content/8: 94412648539ad87efa73bcbab074c988
+ content/9: bcadfc362b69078beee0088e5936c98b
+ content/10: a89c7807e429bb51dd48aa4d2622d0dd
+ content/11: b6d0f2ea216b0007e3c3517a3fa50f1f
+ content/12: 45d60212632f9731ddb5cdb827a515ce
+ content/13: 371d0e46b4bd2c23f559b8bc112f6955
+ content/14: 0d20d22c2c79f1cd78472a3ef7da95bf
+ content/15: bcadfc362b69078beee0088e5936c98b
+ content/16: bccb2c91f666ad27d903f036a75b151e
+ content/17: d2ab825fd4503dbb095177db458d0ff6
+ content/18: f88ad4a7c154ebf76395c29a9955ab94
+ content/19: 371d0e46b4bd2c23f559b8bc112f6955
+ content/20: 7cb6471ac338ca8c1423461eb28a057c
+ content/21: bcadfc362b69078beee0088e5936c98b
+ content/22: 8e5505de3c0649f0a9fd2881a040e409
+ content/23: 1d5cf68c4490f3c5cabb2504eecddb5b
+ content/24: 34d6df7a1bf901b2207a52db746a50f2
+ content/25: 371d0e46b4bd2c23f559b8bc112f6955
+ content/26: 862891a65f07cf068ddbaf046b991f9a
+ content/27: bcadfc362b69078beee0088e5936c98b
+ content/28: 528e47881ef5db3c680d46e80e55f2d6
+ content/29: bc4cb64a528959a7374e1b402f122dfc
+ content/30: 77e2592a86dc0ca50e4db95d808be140
+ content/31: 371d0e46b4bd2c23f559b8bc112f6955
+ content/32: dea2761ea8687fa90bd2d0fcef2fda0d
+ content/33: bcadfc362b69078beee0088e5936c98b
+ content/34: a617913c5160e5e3ce253e2c7ca82dc5
+ content/35: 33708ed18682a449e15de7f9b576d5f4
+ content/36: a2f0f91ad5d9e03fd823ed21373b379b
+ content/37: 371d0e46b4bd2c23f559b8bc112f6955
+ content/38: ec6f414795d5257cb7eb66c5018533b2
+ content/39: bcadfc362b69078beee0088e5936c98b
+ content/40: 5e5a3cd4cbc4ee48f0a67c3664258fb2
+ content/41: a78519a6d0969da4eb60984f1c50de03
+ content/42: 1da9ef7f65dba2f4f0d2b1aa9ddb0ccc
+ content/43: 371d0e46b4bd2c23f559b8bc112f6955
+ content/44: 31c757a9587fae5c97f938d711e7887b
+ content/45: bcadfc362b69078beee0088e5936c98b
+ content/46: d95ba48f24360d807d155a6f8a5bb0be
+ content/47: 3debe47c548eabd98c556240e9d1d573
+ content/48: 0a55d3ddc8c8edfdf1f372f77ad5e231
+ content/49: 371d0e46b4bd2c23f559b8bc112f6955
+ content/50: 7cf573d553835bd8e716190488602db4
+ content/51: bcadfc362b69078beee0088e5936c98b
+ content/52: bf998d73f67b41c2d9a52bc6a2245179
+ content/53: 47b1f90d885890f4a9f7d2eb1e4a1eb2
+ content/54: 3a0804975c50bb33b204c133ae4c4da2
+ content/55: 371d0e46b4bd2c23f559b8bc112f6955
+ content/56: 67238743859f40058d7e4328c6bd072f
+ content/57: bcadfc362b69078beee0088e5936c98b
+ content/58: 9e71ffac1d3e6fa23250d1bca33fdd50
+ content/59: ef72212f9c155dcdf3a98bc4a369ee09
+ content/60: 3fce53203dda68c2d1f9dc901a44b747
+ content/61: 371d0e46b4bd2c23f559b8bc112f6955
+ content/62: 2815e96f98e47e7f7e5b16f68197f084
+ content/63: bcadfc362b69078beee0088e5936c98b
+ content/64: 65890385f788ca17597ce661951fa756
+ content/65: 8228362e54f2a2434467447d7d8075fa
+ content/66: fe2846cd82fcd2515d3c7ad83b50141b
+ content/67: 371d0e46b4bd2c23f559b8bc112f6955
+ content/68: b7afc8fa3b22ea9327e336f50b82a27c
+ content/69: bcadfc362b69078beee0088e5936c98b
+ content/70: 0337e5d7f0bad113be176419350a41b6
+ content/71: ef61f2bab8cfd25a5228d9df3ff6cf3c
+ content/72: 35a991daf9336e6bba2bd8818dd66594
+ content/73: 371d0e46b4bd2c23f559b8bc112f6955
+ content/74: 13644af4a2d5aea5061e9945e91f5a4f
+ content/75: bcadfc362b69078beee0088e5936c98b
+ content/76: f3871a9f36a24d642b6de144d605197a
+ content/77: e301551365a6f7fade24239df33463cd
+ content/78: 46100cc58e4f8c1a4c742c1a5e970d0d
+ content/79: 371d0e46b4bd2c23f559b8bc112f6955
+ content/80: 15e6d6b333eb6a7937028fb886a77e7c
+ content/81: bcadfc362b69078beee0088e5936c98b
+ content/82: 644cb1bcde4e6f1e786b609e74ce43f3
+ content/83: ae715f7048477268f09b33335cf4be93
+ content/84: be625f1454ab49d0eeedb8c2525d8fee
+ content/85: 371d0e46b4bd2c23f559b8bc112f6955
+ content/86: b4143bf400f52a970c236796fdf9fd03
+ content/87: bcadfc362b69078beee0088e5936c98b
+ content/88: 24af8db55301ef64e8d1bcb53b0a5131
+ content/89: 1d3e6443f80e5a643ff2a4a59544e727
+ content/90: 89dca3d2312aadf8b5cc015e0c84e3eb
+ content/91: 371d0e46b4bd2c23f559b8bc112f6955
+ content/92: 862891a65f07cf068ddbaf046b991f9a
+ content/93: bcadfc362b69078beee0088e5936c98b
+ content/94: c365464e4cdae303e61cfc38e35887a0
+ content/95: 0afd6c6ee3ecf06afeea0aaa22b19d8e
+ content/96: d78424fb20ea8b940186b2e0ef0fac55
+ content/97: 371d0e46b4bd2c23f559b8bc112f6955
+ content/98: 4fba9291868e706f90871cdfe0bd2dd7
+ content/99: bcadfc362b69078beee0088e5936c98b
+ content/100: dbd4a81d93749c9c9265b64aff763d93
+ content/101: 1b209b190d6de2af90453ddf6952f480
+ content/102: cec7894f0f14602581914ad3a173ce43
+ content/103: 371d0e46b4bd2c23f559b8bc112f6955
+ content/104: b4143bf400f52a970c236796fdf9fd03
+ content/105: bcadfc362b69078beee0088e5936c98b
+ content/106: 83e0edf0ff07b297aab822804e185af7
+ content/107: 99e6993e88f7da71bf8e63db3bf2d07f
+ content/108: aefc3699ebb31a0046c6480586e66b5b
+ content/109: 371d0e46b4bd2c23f559b8bc112f6955
+ content/110: 0d8f09023861380195cba39d5c78ddc5
+ content/111: bcadfc362b69078beee0088e5936c98b
+ content/112: 94455a7b04b702657ae1e68207d70bb9
+ content/113: 95dc72e389fd1b9a8db155882437a5ef
+ content/114: cadf3a887a76a7a268eb8292d26c8cfd
+ content/115: 371d0e46b4bd2c23f559b8bc112f6955
+ content/116: b4143bf400f52a970c236796fdf9fd03
+ content/117: bcadfc362b69078beee0088e5936c98b
+ content/118: c034d82e3afd5eb98e52b6d46db211f8
+ content/119: a5634cdb8889310cdb3d308a2352e150
+ content/120: 8349f804b5bc2a5580197e4fd848270e
+ content/121: 371d0e46b4bd2c23f559b8bc112f6955
+ content/122: d9576628d658089f122e5fafb200c34c
+ content/123: bcadfc362b69078beee0088e5936c98b
+ content/124: 7be2eecb48d34398e118f223e7947adc
+ content/125: b3f310d5ef115bea5a8b75bf25d7ea9a
+ content/126: b6825d890bccce15d748ceb727d30104
+ f3ceca041234b3c5122b03bc11e4d1c1:
+ meta/title: 960685e215a11e2b38285dff5b0dde47
+ meta/description: 671e4d9e7ed6dd8b7774dcd4cfbecade
+ content/0: 833213d7266febc5d9ac218725cfb057
+ content/1: 10dedb2b36131c07ec48f97ece922c8c
+ content/2: b082096b0c871b2a40418e479af6f158
+ content/3: 9c94aa34f44540b0632931a8244a6488
+ content/4: 14f33e16b5a98e4dbdda2a27aa0d7afb
+ content/5: d7b36732970b7649dd1aa1f1d0a34e74
+ content/6: f554f833467a6dae5391372fc41dad53
+ content/7: 9cdb9189ecfcc4a6f567d3fd5fe342f0
+ content/8: 9a107692cb52c284c1cb022b516d700b
+ content/9: 07a013a9b263ab0ae4458db97065bdcd
+ content/10: 9310a48f3e485c5709563f1b825eb32d
+ content/11: 8a2c3d1a1a30e3614ada44b88478cc0c
+ content/12: defcb9a4ec64b567f45c3669c214763f
+ content/13: 4f3202eff0734a7398445d8c54f9e3ad
+ content/14: afcee4eacb27fb678e159c512d114c2d
+ content/15: 4ecff63a3571ef6f519a2448931451c2
+ content/16: 880b1c60228a0b56c5eb62dac87792df
+ content/17: d3f79ae3be3fe3ca4df5bd59be6b404c
+ content/18: 028eb92d4776faeb029995cee976bfc4
+ content/19: a618fcff50c4856113428639359a922b
+ content/20: 5fd3a6d2dcd8aa18dbf0b784acaa271c
+ content/21: d118656dd565c4c22f3c0c3a7c7f3bee
+ content/22: f49b9be78f1e7a569e290acc1365d417
+ content/23: 0a70ebe6eb4c543c3810977ed46b69b0
+ content/24: ad8638a3473c909dbcb1e1d9f4f26381
+ content/25: 95343a9f81cd050d3713988c677c750f
+ content/26: d4f846a591ac7fdedaba281b44d50ae3
+ content/27: 764eb0e5d025b68f772d45adb7608349
+ content/28: 47eb215a0fc230dc651b7bc05ab25ed0
+ content/29: bf5c6bf1e75c5c5e3a0a5dd1314cb41e
diff --git a/apps/docs/public/static/blocks/triggers.png b/apps/docs/public/static/blocks/triggers.png
index fdcf21d811..43c1b4e3ee 100644
Binary files a/apps/docs/public/static/blocks/triggers.png and b/apps/docs/public/static/blocks/triggers.png differ
diff --git a/apps/docs/public/static/environment/environment-1.png b/apps/docs/public/static/environment/environment-1.png
index 4e5957f651..6a2eded627 100644
Binary files a/apps/docs/public/static/environment/environment-1.png and b/apps/docs/public/static/environment/environment-1.png differ
diff --git a/apps/docs/public/static/environment/environment-2.png b/apps/docs/public/static/environment/environment-2.png
index edd28301e5..3f7617cece 100644
Binary files a/apps/docs/public/static/environment/environment-2.png and b/apps/docs/public/static/environment/environment-2.png differ
diff --git a/apps/docs/public/static/environment/environment-3.png b/apps/docs/public/static/environment/environment-3.png
index f2850e47d6..29ae2e1d1c 100644
Binary files a/apps/docs/public/static/environment/environment-3.png and b/apps/docs/public/static/environment/environment-3.png differ
diff --git a/apps/docs/public/static/knowledgebase/knowledgebase.png b/apps/docs/public/static/knowledgebase/knowledgebase.png
index 9fd1954156..a938385f57 100644
Binary files a/apps/docs/public/static/knowledgebase/knowledgebase.png and b/apps/docs/public/static/knowledgebase/knowledgebase.png differ
diff --git a/apps/sim/app/(landing)/careers/page.tsx b/apps/sim/app/(landing)/careers/page.tsx
index 37c5f7a0e1..d756661aa1 100644
--- a/apps/sim/app/(landing)/careers/page.tsx
+++ b/apps/sim/app/(landing)/careers/page.tsx
@@ -3,6 +3,7 @@
import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { X } from 'lucide-react'
+import { Textarea } from '@/components/emcn'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -13,7 +14,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
-import { Textarea } from '@/components/ui/textarea'
import { isHosted } from '@/lib/core/config/feature-flags'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
diff --git a/apps/sim/app/(landing)/studio/[slug]/page.tsx b/apps/sim/app/(landing)/studio/[slug]/page.tsx
index 531cebd52f..77362a0554 100644
--- a/apps/sim/app/(landing)/studio/[slug]/page.tsx
+++ b/apps/sim/app/(landing)/studio/[slug]/page.tsx
@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
import { FAQ } from '@/lib/blog/faq'
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
diff --git a/apps/sim/app/(landing)/studio/post-grid.tsx b/apps/sim/app/(landing)/studio/post-grid.tsx
index 22c49032bc..69b0562621 100644
--- a/apps/sim/app/(landing)/studio/post-grid.tsx
+++ b/apps/sim/app/(landing)/studio/post-grid.tsx
@@ -2,7 +2,7 @@
import Image from 'next/image'
import Link from 'next/link'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
interface Author {
id: string
diff --git a/apps/sim/app/_shell/providers/theme-provider.tsx b/apps/sim/app/_shell/providers/theme-provider.tsx
index cbb31e4423..dae3071b5c 100644
--- a/apps/sim/app/_shell/providers/theme-provider.tsx
+++ b/apps/sim/app/_shell/providers/theme-provider.tsx
@@ -21,12 +21,13 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
pathname.startsWith('/careers') ||
pathname.startsWith('/changelog') ||
pathname.startsWith('/chat') ||
- pathname.startsWith('/studio')
+ pathname.startsWith('/studio') ||
+ pathname.startsWith('/resume')
return (
w.entityId)
+
+ if (workspaceIds.length === 0) {
+ return NextResponse.json({ success: true, servers: [] })
+ }
+
+ const servers = await db
+ .select({
+ id: workflowMcpServer.id,
+ name: workflowMcpServer.name,
+ description: workflowMcpServer.description,
+ workspaceId: workflowMcpServer.workspaceId,
+ workspaceName: workspace.name,
+ createdAt: workflowMcpServer.createdAt,
+ toolCount: sql`(
+ SELECT COUNT(*)::int
+ FROM "workflow_mcp_tool"
+ WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
+ )`.as('tool_count'),
+ })
+ .from(workflowMcpServer)
+ .leftJoin(workspace, eq(workflowMcpServer.workspaceId, workspace.id))
+ .where(sql`${workflowMcpServer.workspaceId} IN ${workspaceIds}`)
+ .orderBy(workflowMcpServer.name)
+
+ const baseUrl = getBaseUrl()
+
+ const formattedServers = servers.map((server) => ({
+ id: server.id,
+ name: server.name,
+ description: server.description,
+ workspace: { id: server.workspaceId, name: server.workspaceName },
+ toolCount: server.toolCount || 0,
+ createdAt: server.createdAt,
+ url: `${baseUrl}/api/mcp/serve/${server.id}`,
+ }))
+
+ logger.info(`User ${userId} discovered ${formattedServers.length} MCP servers`)
+
+ return NextResponse.json({
+ success: true,
+ servers: formattedServers,
+ authentication: {
+ method: 'API Key',
+ header: 'X-API-Key',
+ },
+ })
+ } catch (error) {
+ logger.error('Error discovering MCP servers:', error)
+ return NextResponse.json(
+ { success: false, error: 'Failed to discover MCP servers' },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.ts
new file mode 100644
index 0000000000..cc9ec0272f
--- /dev/null
+++ b/apps/sim/app/api/mcp/serve/[serverId]/route.ts
@@ -0,0 +1,306 @@
+/**
+ * MCP Serve Endpoint - Implements MCP protocol for workflow servers using SDK types.
+ */
+
+import {
+ type CallToolResult,
+ ErrorCode,
+ type InitializeResult,
+ isJSONRPCNotification,
+ isJSONRPCRequest,
+ type JSONRPCError,
+ type JSONRPCMessage,
+ type JSONRPCResponse,
+ type ListToolsResult,
+ type RequestId,
+} from '@modelcontextprotocol/sdk/types.js'
+import { db } from '@sim/db'
+import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { and, eq } from 'drizzle-orm'
+import { type NextRequest, NextResponse } from 'next/server'
+import { checkHybridAuth } from '@/lib/auth/hybrid'
+import { getBaseUrl } from '@/lib/core/utils/urls'
+
+const logger = createLogger('WorkflowMcpServeAPI')
+
+export const dynamic = 'force-dynamic'
+
+interface RouteParams {
+ serverId: string
+}
+
+function createResponse(id: RequestId, result: unknown): JSONRPCResponse {
+ return {
+ jsonrpc: '2.0',
+ id,
+ result: result as JSONRPCResponse['result'],
+ }
+}
+
+function createError(id: RequestId, code: ErrorCode | number, message: string): JSONRPCError {
+ return {
+ jsonrpc: '2.0',
+ id,
+ error: { code, message },
+ }
+}
+
+async function getServer(serverId: string) {
+ const [server] = await db
+ .select({
+ id: workflowMcpServer.id,
+ name: workflowMcpServer.name,
+ workspaceId: workflowMcpServer.workspaceId,
+ })
+ .from(workflowMcpServer)
+ .where(eq(workflowMcpServer.id, serverId))
+ .limit(1)
+
+ return server
+}
+
+export async function GET(request: NextRequest, { params }: { params: Promise }) {
+ const { serverId } = await params
+
+ try {
+ const server = await getServer(serverId)
+ if (!server) {
+ return NextResponse.json({ error: 'Server not found' }, { status: 404 })
+ }
+
+ return NextResponse.json({
+ name: server.name,
+ version: '1.0.0',
+ protocolVersion: '2024-11-05',
+ capabilities: { tools: {} },
+ })
+ } catch (error) {
+ logger.error('Error getting MCP server info:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
+
+export async function POST(request: NextRequest, { params }: { params: Promise }) {
+ const { serverId } = await params
+
+ try {
+ const server = await getServer(serverId)
+ if (!server) {
+ return NextResponse.json({ error: 'Server not found' }, { status: 404 })
+ }
+
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success || !auth.userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const body = await request.json()
+ const message = body as JSONRPCMessage
+
+ if (isJSONRPCNotification(message)) {
+ logger.info(`Received notification: ${message.method}`)
+ return new NextResponse(null, { status: 202 })
+ }
+
+ if (!isJSONRPCRequest(message)) {
+ return NextResponse.json(
+ createError(0, ErrorCode.InvalidRequest, 'Invalid JSON-RPC message'),
+ {
+ status: 400,
+ }
+ )
+ }
+
+ const { id, method, params: rpcParams } = message
+ const apiKey =
+ request.headers.get('X-API-Key') ||
+ request.headers.get('Authorization')?.replace('Bearer ', '')
+
+ switch (method) {
+ case 'initialize': {
+ const result: InitializeResult = {
+ protocolVersion: '2024-11-05',
+ capabilities: { tools: {} },
+ serverInfo: { name: server.name, version: '1.0.0' },
+ }
+ return NextResponse.json(createResponse(id, result))
+ }
+
+ case 'ping':
+ return NextResponse.json(createResponse(id, {}))
+
+ case 'tools/list':
+ return handleToolsList(id, serverId)
+
+ case 'tools/call':
+ return handleToolsCall(
+ id,
+ serverId,
+ rpcParams as { name: string; arguments?: Record },
+ apiKey
+ )
+
+ default:
+ return NextResponse.json(
+ createError(id, ErrorCode.MethodNotFound, `Method not found: ${method}`),
+ {
+ status: 404,
+ }
+ )
+ }
+ } catch (error) {
+ logger.error('Error handling MCP request:', error)
+ return NextResponse.json(createError(0, ErrorCode.InternalError, 'Internal error'), {
+ status: 500,
+ })
+ }
+}
+
+async function handleToolsList(id: RequestId, serverId: string): Promise {
+ try {
+ const tools = await db
+ .select({
+ toolName: workflowMcpTool.toolName,
+ toolDescription: workflowMcpTool.toolDescription,
+ parameterSchema: workflowMcpTool.parameterSchema,
+ })
+ .from(workflowMcpTool)
+ .where(eq(workflowMcpTool.serverId, serverId))
+
+ const result: ListToolsResult = {
+ tools: tools.map((tool) => {
+ const schema = tool.parameterSchema as {
+ type?: string
+ properties?: Record
+ required?: string[]
+ } | null
+ return {
+ name: tool.toolName,
+ description: tool.toolDescription || `Execute workflow: ${tool.toolName}`,
+ inputSchema: {
+ type: 'object' as const,
+ properties: schema?.properties || {},
+ ...(schema?.required && schema.required.length > 0 && { required: schema.required }),
+ },
+ }
+ }),
+ }
+
+ return NextResponse.json(createResponse(id, result))
+ } catch (error) {
+ logger.error('Error listing tools:', error)
+ return NextResponse.json(createError(id, ErrorCode.InternalError, 'Failed to list tools'), {
+ status: 500,
+ })
+ }
+}
+
+async function handleToolsCall(
+ id: RequestId,
+ serverId: string,
+ params: { name: string; arguments?: Record } | undefined,
+ apiKey?: string | null
+): Promise {
+ try {
+ if (!params?.name) {
+ return NextResponse.json(createError(id, ErrorCode.InvalidParams, 'Tool name required'), {
+ status: 400,
+ })
+ }
+
+ const [tool] = await db
+ .select({
+ toolName: workflowMcpTool.toolName,
+ workflowId: workflowMcpTool.workflowId,
+ })
+ .from(workflowMcpTool)
+ .where(and(eq(workflowMcpTool.serverId, serverId), eq(workflowMcpTool.toolName, params.name)))
+ .limit(1)
+ if (!tool) {
+ return NextResponse.json(
+ createError(id, ErrorCode.InvalidParams, `Tool not found: ${params.name}`),
+ {
+ status: 404,
+ }
+ )
+ }
+
+ const [wf] = await db
+ .select({ isDeployed: workflow.isDeployed })
+ .from(workflow)
+ .where(eq(workflow.id, tool.workflowId))
+ .limit(1)
+
+ if (!wf?.isDeployed) {
+ return NextResponse.json(
+ createError(id, ErrorCode.InternalError, 'Workflow is not deployed'),
+ {
+ status: 400,
+ }
+ )
+ }
+
+ const executeUrl = `${getBaseUrl()}/api/workflows/${tool.workflowId}/execute`
+ const headers: Record = { 'Content-Type': 'application/json' }
+ if (apiKey) headers['X-API-Key'] = apiKey
+
+ logger.info(`Executing workflow ${tool.workflowId} via MCP tool ${params.name}`)
+
+ const response = await fetch(executeUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }),
+ signal: AbortSignal.timeout(300000), // 5 minute timeout
+ })
+
+ const executeResult = await response.json()
+
+ if (!response.ok) {
+ return NextResponse.json(
+ createError(
+ id,
+ ErrorCode.InternalError,
+ executeResult.error || 'Workflow execution failed'
+ ),
+ { status: 500 }
+ )
+ }
+
+ const result: CallToolResult = {
+ content: [
+ { type: 'text', text: JSON.stringify(executeResult.output || executeResult, null, 2) },
+ ],
+ isError: !executeResult.success,
+ }
+
+ return NextResponse.json(createResponse(id, result))
+ } catch (error) {
+ logger.error('Error calling tool:', error)
+ return NextResponse.json(createError(id, ErrorCode.InternalError, 'Tool execution failed'), {
+ status: 500,
+ })
+ }
+}
+
+export async function DELETE(request: NextRequest, { params }: { params: Promise }) {
+ const { serverId } = await params
+
+ try {
+ const server = await getServer(serverId)
+ if (!server) {
+ return NextResponse.json({ error: 'Server not found' }, { status: 404 })
+ }
+
+ const auth = await checkHybridAuth(request, { requireWorkflowId: false })
+ if (!auth.success || !auth.userId) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ logger.info(`MCP session terminated for server ${serverId}`)
+ return new NextResponse(null, { status: 204 })
+ } catch (error) {
+ logger.error('Error handling MCP DELETE request:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
diff --git a/apps/sim/app/api/mcp/servers/[id]/refresh/route.ts b/apps/sim/app/api/mcp/servers/[id]/refresh/route.ts
index 2e3474e68d..94348a0f73 100644
--- a/apps/sim/app/api/mcp/servers/[id]/refresh/route.ts
+++ b/apps/sim/app/api/mcp/servers/[id]/refresh/route.ts
@@ -1,31 +1,150 @@
import { db } from '@sim/db'
-import { mcpServers } from '@sim/db/schema'
+import { mcpServers, workflow, workflowBlocks } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
-import type { McpServerStatusConfig } from '@/lib/mcp/types'
-import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
+import type { McpServerStatusConfig, McpTool, McpToolSchema } from '@/lib/mcp/types'
+import {
+ createMcpErrorResponse,
+ createMcpSuccessResponse,
+ MCP_TOOL_CORE_PARAMS,
+} from '@/lib/mcp/utils'
const logger = createLogger('McpServerRefreshAPI')
export const dynamic = 'force-dynamic'
+/** Schema stored in workflow blocks includes description from the tool. */
+type StoredToolSchema = McpToolSchema & { description?: string }
+
+interface StoredTool {
+ type: string
+ title: string
+ toolId: string
+ params: {
+ serverId: string
+ serverUrl?: string
+ toolName: string
+ serverName?: string
+ }
+ schema?: StoredToolSchema
+ [key: string]: unknown
+}
+
+interface SyncResult {
+ updatedCount: number
+ updatedWorkflowIds: string[]
+}
+
/**
- * POST - Refresh an MCP server connection (requires any workspace permission)
+ * Syncs tool schemas from discovered MCP tools to all workflow blocks using those tools.
+ * Returns the count and IDs of updated workflows.
*/
+async function syncToolSchemasToWorkflows(
+ workspaceId: string,
+ serverId: string,
+ tools: McpTool[],
+ requestId: string
+): Promise {
+ const toolsByName = new Map(tools.map((t) => [t.name, t]))
+
+ const workspaceWorkflows = await db
+ .select({ id: workflow.id })
+ .from(workflow)
+ .where(eq(workflow.workspaceId, workspaceId))
+
+ const workflowIds = workspaceWorkflows.map((w) => w.id)
+ if (workflowIds.length === 0) return { updatedCount: 0, updatedWorkflowIds: [] }
+
+ const agentBlocks = await db
+ .select({
+ id: workflowBlocks.id,
+ workflowId: workflowBlocks.workflowId,
+ subBlocks: workflowBlocks.subBlocks,
+ })
+ .from(workflowBlocks)
+ .where(eq(workflowBlocks.type, 'agent'))
+
+ const updatedWorkflowIds = new Set()
+
+ for (const block of agentBlocks) {
+ if (!workflowIds.includes(block.workflowId)) continue
+
+ const subBlocks = block.subBlocks as Record | null
+ if (!subBlocks) continue
+
+ const toolsSubBlock = subBlocks.tools as { value?: StoredTool[] } | undefined
+ if (!toolsSubBlock?.value || !Array.isArray(toolsSubBlock.value)) continue
+
+ let hasUpdates = false
+ const updatedTools = toolsSubBlock.value.map((tool) => {
+ if (tool.type !== 'mcp' || tool.params?.serverId !== serverId) {
+ return tool
+ }
+
+ const freshTool = toolsByName.get(tool.params.toolName)
+ if (!freshTool) return tool
+
+ const newSchema: StoredToolSchema = {
+ ...freshTool.inputSchema,
+ description: freshTool.description,
+ }
+
+ const schemasMatch = JSON.stringify(tool.schema) === JSON.stringify(newSchema)
+
+ if (!schemasMatch) {
+ hasUpdates = true
+
+ const validParamKeys = new Set(Object.keys(newSchema.properties || {}))
+
+ const cleanedParams: Record = {}
+ for (const [key, value] of Object.entries(tool.params || {})) {
+ if (MCP_TOOL_CORE_PARAMS.has(key) || validParamKeys.has(key)) {
+ cleanedParams[key] = value
+ }
+ }
+
+ return { ...tool, schema: newSchema, params: cleanedParams }
+ }
+
+ return tool
+ })
+
+ if (hasUpdates) {
+ const updatedSubBlocks = {
+ ...subBlocks,
+ tools: { ...toolsSubBlock, value: updatedTools },
+ }
+
+ await db
+ .update(workflowBlocks)
+ .set({ subBlocks: updatedSubBlocks, updatedAt: new Date() })
+ .where(eq(workflowBlocks.id, block.id))
+
+ updatedWorkflowIds.add(block.workflowId)
+ }
+ }
+
+ if (updatedWorkflowIds.size > 0) {
+ logger.info(
+ `[${requestId}] Synced tool schemas to ${updatedWorkflowIds.size} workflow(s) for server ${serverId}`
+ )
+ }
+
+ return {
+ updatedCount: updatedWorkflowIds.size,
+ updatedWorkflowIds: Array.from(updatedWorkflowIds),
+ }
+}
+
export const POST = withMcpAuth<{ id: string }>('read')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
const { id: serverId } = await params
try {
- logger.info(
- `[${requestId}] Refreshing MCP server: ${serverId} in workspace: ${workspaceId}`,
- {
- userId,
- }
- )
+ logger.info(`[${requestId}] Refreshing MCP server: ${serverId}`)
const [server] = await db
.select()
@@ -50,6 +169,8 @@ export const POST = withMcpAuth<{ id: string }>('read')(
let connectionStatus: 'connected' | 'disconnected' | 'error' = 'error'
let toolCount = 0
let lastError: string | null = null
+ let syncResult: SyncResult = { updatedCount: 0, updatedWorkflowIds: [] }
+ let discoveredTools: McpTool[] = []
const currentStatusConfig: McpServerStatusConfig =
(server.statusConfig as McpServerStatusConfig | null) ?? {
@@ -58,11 +179,16 @@ export const POST = withMcpAuth<{ id: string }>('read')(
}
try {
- const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
+ discoveredTools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
connectionStatus = 'connected'
- toolCount = tools.length
- logger.info(
- `[${requestId}] Successfully connected to server ${serverId}, discovered ${toolCount} tools`
+ toolCount = discoveredTools.length
+ logger.info(`[${requestId}] Discovered ${toolCount} tools from server ${serverId}`)
+
+ syncResult = await syncToolSchemasToWorkflows(
+ workspaceId,
+ serverId,
+ discoveredTools,
+ requestId
)
} catch (error) {
connectionStatus = 'error'
@@ -94,14 +220,7 @@ export const POST = withMcpAuth<{ id: string }>('read')(
.returning()
if (connectionStatus === 'connected') {
- logger.info(
- `[${requestId}] Successfully refreshed MCP server: ${serverId} (${toolCount} tools)`
- )
await mcpService.clearCache(workspaceId)
- } else {
- logger.warn(
- `[${requestId}] Refresh completed for MCP server ${serverId} but connection failed: ${lastError}`
- )
}
return createMcpSuccessResponse({
@@ -109,6 +228,8 @@ export const POST = withMcpAuth<{ id: string }>('read')(
toolCount,
lastConnected: refreshedServer?.lastConnected?.toISOString() || null,
error: lastError,
+ workflowsUpdated: syncResult.updatedCount,
+ updatedWorkflowIds: syncResult.updatedWorkflowIds,
})
} catch (error) {
logger.error(`[${requestId}] Error refreshing MCP server:`, error)
diff --git a/apps/sim/app/api/mcp/servers/[id]/route.ts b/apps/sim/app/api/mcp/servers/[id]/route.ts
index fc986ccc9f..e7b2d9f1d3 100644
--- a/apps/sim/app/api/mcp/servers/[id]/route.ts
+++ b/apps/sim/app/api/mcp/servers/[id]/route.ts
@@ -5,7 +5,6 @@ import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
-import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
const logger = createLogger('McpServerAPI')
@@ -27,24 +26,6 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
updates: Object.keys(body).filter((k) => k !== 'workspaceId'),
})
- // Validate URL if being updated
- if (
- body.url &&
- (body.transport === 'http' ||
- body.transport === 'sse' ||
- body.transport === 'streamable-http')
- ) {
- const urlValidation = validateMcpServerUrl(body.url)
- if (!urlValidation.isValid) {
- return createMcpErrorResponse(
- new Error(`Invalid MCP server URL: ${urlValidation.error}`),
- 'Invalid server URL',
- 400
- )
- }
- body.url = urlValidation.normalizedUrl
- }
-
// Remove workspaceId from body to prevent it from being updated
const { workspaceId: _, ...updateData } = body
diff --git a/apps/sim/app/api/mcp/servers/route.ts b/apps/sim/app/api/mcp/servers/route.ts
index d8ca7c93ff..4ba367d133 100644
--- a/apps/sim/app/api/mcp/servers/route.ts
+++ b/apps/sim/app/api/mcp/servers/route.ts
@@ -5,8 +5,6 @@ import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
-import type { McpTransport } from '@/lib/mcp/types'
-import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
import {
createMcpErrorResponse,
createMcpSuccessResponse,
@@ -17,13 +15,6 @@ const logger = createLogger('McpServersAPI')
export const dynamic = 'force-dynamic'
-/**
- * Check if transport type requires a URL
- */
-function isUrlBasedTransport(transport: McpTransport): boolean {
- return transport === 'streamable-http'
-}
-
/**
* GET - List all registered MCP servers for the workspace
*/
@@ -81,18 +72,6 @@ export const POST = withMcpAuth('write')(
)
}
- if (isUrlBasedTransport(body.transport) && body.url) {
- const urlValidation = validateMcpServerUrl(body.url)
- if (!urlValidation.isValid) {
- return createMcpErrorResponse(
- new Error(`Invalid MCP server URL: ${urlValidation.error}`),
- 'Invalid server URL',
- 400
- )
- }
- body.url = urlValidation.normalizedUrl
- }
-
const serverId = body.url ? generateMcpServerId(workspaceId, body.url) : crypto.randomUUID()
const [existingServer] = await db
diff --git a/apps/sim/app/api/mcp/servers/test-connection/route.ts b/apps/sim/app/api/mcp/servers/test-connection/route.ts
index cc52ec88e4..3332397535 100644
--- a/apps/sim/app/api/mcp/servers/test-connection/route.ts
+++ b/apps/sim/app/api/mcp/servers/test-connection/route.ts
@@ -4,7 +4,6 @@ import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
import { McpClient } from '@/lib/mcp/client'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
-import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
import { REFERENCE } from '@/executor/constants'
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
@@ -89,24 +88,12 @@ export const POST = withMcpAuth('write')(
)
}
- if (isUrlBasedTransport(body.transport)) {
- if (!body.url) {
- return createMcpErrorResponse(
- new Error('URL is required for HTTP-based transports'),
- 'Missing required URL',
- 400
- )
- }
-
- const urlValidation = validateMcpServerUrl(body.url)
- if (!urlValidation.isValid) {
- return createMcpErrorResponse(
- new Error(`Invalid MCP server URL: ${urlValidation.error}`),
- 'Invalid server URL',
- 400
- )
- }
- body.url = urlValidation.normalizedUrl
+ if (isUrlBasedTransport(body.transport) && !body.url) {
+ return createMcpErrorResponse(
+ new Error('URL is required for HTTP-based transports'),
+ 'Missing required URL',
+ 400
+ )
}
let resolvedUrl = body.url
diff --git a/apps/sim/app/api/mcp/tools/discover/route.ts b/apps/sim/app/api/mcp/tools/discover/route.ts
index de88cbb28b..b62470274a 100644
--- a/apps/sim/app/api/mcp/tools/discover/route.ts
+++ b/apps/sim/app/api/mcp/tools/discover/route.ts
@@ -9,9 +9,6 @@ const logger = createLogger('McpToolDiscoveryAPI')
export const dynamic = 'force-dynamic'
-/**
- * GET - Discover all tools from user's MCP servers
- */
export const GET = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
@@ -19,18 +16,11 @@ export const GET = withMcpAuth('read')(
const serverId = searchParams.get('serverId')
const forceRefresh = searchParams.get('refresh') === 'true'
- logger.info(`[${requestId}] Discovering MCP tools for user ${userId}`, {
- serverId,
- workspaceId,
- forceRefresh,
- })
+ logger.info(`[${requestId}] Discovering MCP tools`, { serverId, workspaceId, forceRefresh })
- let tools
- if (serverId) {
- tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
- } else {
- tools = await mcpService.discoverTools(userId, workspaceId, forceRefresh)
- }
+ const tools = serverId
+ ? await mcpService.discoverServerTools(userId, serverId, workspaceId)
+ : await mcpService.discoverTools(userId, workspaceId, forceRefresh)
const byServer: Record = {}
for (const tool of tools) {
@@ -55,9 +45,6 @@ export const GET = withMcpAuth('read')(
}
)
-/**
- * POST - Refresh tool discovery for specific servers
- */
export const POST = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
@@ -72,10 +59,7 @@ export const POST = withMcpAuth('read')(
)
}
- logger.info(
- `[${requestId}] Refreshing tool discovery for user ${userId}, servers:`,
- serverIds
- )
+ logger.info(`[${requestId}] Refreshing tools for ${serverIds.length} servers`)
const results = await Promise.allSettled(
serverIds.map(async (serverId: string) => {
@@ -99,7 +83,8 @@ export const POST = withMcpAuth('read')(
}
})
- const responseData = {
+ logger.info(`[${requestId}] Refresh completed: ${successes.length}/${serverIds.length}`)
+ return createMcpSuccessResponse({
refreshed: successes,
failed: failures,
summary: {
@@ -107,12 +92,7 @@ export const POST = withMcpAuth('read')(
successful: successes.length,
failed: failures.length,
},
- }
-
- logger.info(
- `[${requestId}] Tool discovery refresh completed: ${successes.length}/${serverIds.length} successful`
- )
- return createMcpSuccessResponse(responseData)
+ })
} catch (error) {
logger.error(`[${requestId}] Error refreshing tool discovery:`, error)
const { message, status } = categorizeError(error)
diff --git a/apps/sim/app/api/mcp/tools/stored/route.ts b/apps/sim/app/api/mcp/tools/stored/route.ts
index 09519aa677..5a5519c277 100644
--- a/apps/sim/app/api/mcp/tools/stored/route.ts
+++ b/apps/sim/app/api/mcp/tools/stored/route.ts
@@ -4,39 +4,20 @@ import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { withMcpAuth } from '@/lib/mcp/middleware'
+import type { McpToolSchema, StoredMcpTool } from '@/lib/mcp/types'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
const logger = createLogger('McpStoredToolsAPI')
export const dynamic = 'force-dynamic'
-interface StoredMcpTool {
- workflowId: string
- workflowName: string
- serverId: string
- serverUrl?: string
- toolName: string
- schema?: Record
-}
-
-/**
- * GET - Get all stored MCP tools from workflows in the workspace
- *
- * Scans all workflows in the workspace and extracts MCP tools that have been
- * added to agent blocks. Returns the stored state of each tool for comparison
- * against current server state.
- */
export const GET = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
logger.info(`[${requestId}] Fetching stored MCP tools for workspace ${workspaceId}`)
- // Get all workflows in workspace
const workflows = await db
- .select({
- id: workflow.id,
- name: workflow.name,
- })
+ .select({ id: workflow.id, name: workflow.name })
.from(workflow)
.where(eq(workflow.workspaceId, workspaceId))
@@ -47,12 +28,8 @@ export const GET = withMcpAuth('read')(
return createMcpSuccessResponse({ tools: [] })
}
- // Get all agent blocks from these workflows
const agentBlocks = await db
- .select({
- workflowId: workflowBlocks.workflowId,
- subBlocks: workflowBlocks.subBlocks,
- })
+ .select({ workflowId: workflowBlocks.workflowId, subBlocks: workflowBlocks.subBlocks })
.from(workflowBlocks)
.where(eq(workflowBlocks.type, 'agent'))
@@ -81,7 +58,7 @@ export const GET = withMcpAuth('read')(
serverId: params.serverId as string,
serverUrl: params.serverUrl as string | undefined,
toolName: params.toolName as string,
- schema: tool.schema as Record | undefined,
+ schema: tool.schema as McpToolSchema | undefined,
})
}
}
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
new file mode 100644
index 0000000000..62266b817a
--- /dev/null
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
@@ -0,0 +1,155 @@
+import { db } from '@sim/db'
+import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { and, eq } from 'drizzle-orm'
+import type { NextRequest } from 'next/server'
+import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
+import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
+
+const logger = createLogger('WorkflowMcpServerAPI')
+
+export const dynamic = 'force-dynamic'
+
+interface RouteParams {
+ id: string
+}
+
+/**
+ * GET - Get a specific workflow MCP server with its tools
+ */
+export const GET = withMcpAuth('read')(
+ async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
+ try {
+ const { id: serverId } = await params
+
+ logger.info(`[${requestId}] Getting workflow MCP server: ${serverId}`)
+
+ const [server] = await db
+ .select({
+ id: workflowMcpServer.id,
+ workspaceId: workflowMcpServer.workspaceId,
+ createdBy: workflowMcpServer.createdBy,
+ name: workflowMcpServer.name,
+ description: workflowMcpServer.description,
+ createdAt: workflowMcpServer.createdAt,
+ updatedAt: workflowMcpServer.updatedAt,
+ })
+ .from(workflowMcpServer)
+ .where(
+ and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
+ )
+ .limit(1)
+
+ if (!server) {
+ return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
+ }
+
+ const tools = await db
+ .select()
+ .from(workflowMcpTool)
+ .where(eq(workflowMcpTool.serverId, serverId))
+
+ logger.info(
+ `[${requestId}] Found workflow MCP server: ${server.name} with ${tools.length} tools`
+ )
+
+ return createMcpSuccessResponse({ server, tools })
+ } catch (error) {
+ logger.error(`[${requestId}] Error getting workflow MCP server:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to get workflow MCP server'),
+ 'Failed to get workflow MCP server',
+ 500
+ )
+ }
+ }
+)
+
+/**
+ * PATCH - Update a workflow MCP server
+ */
+export const PATCH = withMcpAuth('write')(
+ async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
+ try {
+ const { id: serverId } = await params
+ const body = getParsedBody(request) || (await request.json())
+
+ logger.info(`[${requestId}] Updating workflow MCP server: ${serverId}`)
+
+ const [existingServer] = await db
+ .select({ id: workflowMcpServer.id })
+ .from(workflowMcpServer)
+ .where(
+ and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
+ )
+ .limit(1)
+
+ if (!existingServer) {
+ return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
+ }
+
+ const updateData: Record = {
+ updatedAt: new Date(),
+ }
+
+ if (body.name !== undefined) {
+ updateData.name = body.name.trim()
+ }
+ if (body.description !== undefined) {
+ updateData.description = body.description?.trim() || null
+ }
+
+ const [updatedServer] = await db
+ .update(workflowMcpServer)
+ .set(updateData)
+ .where(eq(workflowMcpServer.id, serverId))
+ .returning()
+
+ logger.info(`[${requestId}] Successfully updated workflow MCP server: ${serverId}`)
+
+ return createMcpSuccessResponse({ server: updatedServer })
+ } catch (error) {
+ logger.error(`[${requestId}] Error updating workflow MCP server:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to update workflow MCP server'),
+ 'Failed to update workflow MCP server',
+ 500
+ )
+ }
+ }
+)
+
+/**
+ * DELETE - Delete a workflow MCP server and all its tools
+ */
+export const DELETE = withMcpAuth('admin')(
+ async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
+ try {
+ const { id: serverId } = await params
+
+ logger.info(`[${requestId}] Deleting workflow MCP server: ${serverId}`)
+
+ const [deletedServer] = await db
+ .delete(workflowMcpServer)
+ .where(
+ and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
+ )
+ .returning()
+
+ if (!deletedServer) {
+ return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
+ }
+
+ logger.info(`[${requestId}] Successfully deleted workflow MCP server: ${serverId}`)
+
+ return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
+ } catch (error) {
+ logger.error(`[${requestId}] Error deleting workflow MCP server:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to delete workflow MCP server'),
+ 'Failed to delete workflow MCP server',
+ 500
+ )
+ }
+ }
+)
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
new file mode 100644
index 0000000000..4398bd4e53
--- /dev/null
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
@@ -0,0 +1,176 @@
+import { db } from '@sim/db'
+import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { and, eq } from 'drizzle-orm'
+import type { NextRequest } from 'next/server'
+import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
+import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
+import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
+
+const logger = createLogger('WorkflowMcpToolAPI')
+
+export const dynamic = 'force-dynamic'
+
+interface RouteParams {
+ id: string
+ toolId: string
+}
+
+/**
+ * GET - Get a specific tool
+ */
+export const GET = withMcpAuth('read')(
+ async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
+ try {
+ const { id: serverId, toolId } = await params
+
+ logger.info(`[${requestId}] Getting tool ${toolId} from server ${serverId}`)
+
+ // Verify server exists and belongs to workspace
+ const [server] = await db
+ .select({ id: workflowMcpServer.id })
+ .from(workflowMcpServer)
+ .where(
+ and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
+ )
+ .limit(1)
+
+ if (!server) {
+ return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
+ }
+
+ const [tool] = await db
+ .select()
+ .from(workflowMcpTool)
+ .where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
+ .limit(1)
+
+ if (!tool) {
+ return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
+ }
+
+ return createMcpSuccessResponse({ tool })
+ } catch (error) {
+ logger.error(`[${requestId}] Error getting tool:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to get tool'),
+ 'Failed to get tool',
+ 500
+ )
+ }
+ }
+)
+
+/**
+ * PATCH - Update a tool's configuration
+ */
+export const PATCH = withMcpAuth('write')(
+ async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
+ try {
+ const { id: serverId, toolId } = await params
+ const body = getParsedBody(request) || (await request.json())
+
+ logger.info(`[${requestId}] Updating tool ${toolId} in server ${serverId}`)
+
+ // Verify server exists and belongs to workspace
+ const [server] = await db
+ .select({ id: workflowMcpServer.id })
+ .from(workflowMcpServer)
+ .where(
+ and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
+ )
+ .limit(1)
+
+ if (!server) {
+ return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
+ }
+
+ const [existingTool] = await db
+ .select({ id: workflowMcpTool.id })
+ .from(workflowMcpTool)
+ .where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
+ .limit(1)
+
+ if (!existingTool) {
+ return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
+ }
+
+ const updateData: Record = {
+ updatedAt: new Date(),
+ }
+
+ if (body.toolName !== undefined) {
+ updateData.toolName = sanitizeToolName(body.toolName)
+ }
+ if (body.toolDescription !== undefined) {
+ updateData.toolDescription = body.toolDescription?.trim() || null
+ }
+ if (body.parameterSchema !== undefined) {
+ updateData.parameterSchema = body.parameterSchema
+ }
+
+ const [updatedTool] = await db
+ .update(workflowMcpTool)
+ .set(updateData)
+ .where(eq(workflowMcpTool.id, toolId))
+ .returning()
+
+ logger.info(`[${requestId}] Successfully updated tool ${toolId}`)
+
+ return createMcpSuccessResponse({ tool: updatedTool })
+ } catch (error) {
+ logger.error(`[${requestId}] Error updating tool:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to update tool'),
+ 'Failed to update tool',
+ 500
+ )
+ }
+ }
+)
+
+/**
+ * DELETE - Remove a tool from an MCP server
+ */
+export const DELETE = withMcpAuth('write')(
+ async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
+ try {
+ const { id: serverId, toolId } = await params
+
+ logger.info(`[${requestId}] Deleting tool ${toolId} from server ${serverId}`)
+
+ // Verify server exists and belongs to workspace
+ const [server] = await db
+ .select({ id: workflowMcpServer.id })
+ .from(workflowMcpServer)
+ .where(
+ and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
+ )
+ .limit(1)
+
+ if (!server) {
+ return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
+ }
+
+ const [deletedTool] = await db
+ .delete(workflowMcpTool)
+ .where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
+ .returning()
+
+ if (!deletedTool) {
+ return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
+ }
+
+ logger.info(`[${requestId}] Successfully deleted tool ${toolId}`)
+
+ return createMcpSuccessResponse({ message: `Tool ${toolId} deleted successfully` })
+ } catch (error) {
+ logger.error(`[${requestId}] Error deleting tool:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to delete tool'),
+ 'Failed to delete tool',
+ 500
+ )
+ }
+ }
+)
diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
new file mode 100644
index 0000000000..5c39098b0f
--- /dev/null
+++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
@@ -0,0 +1,223 @@
+import { db } from '@sim/db'
+import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { and, eq } from 'drizzle-orm'
+import type { NextRequest } from 'next/server'
+import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
+import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
+import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
+import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
+
+const logger = createLogger('WorkflowMcpToolsAPI')
+
+/**
+ * Check if a workflow has a valid start block by loading from database
+ */
+async function hasValidStartBlock(workflowId: string): Promise {
+ try {
+ const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
+ return hasValidStartBlockInState(normalizedData)
+ } catch (error) {
+ logger.warn('Error checking for start block:', error)
+ return false
+ }
+}
+
+export const dynamic = 'force-dynamic'
+
+interface RouteParams {
+ id: string
+}
+
+/**
+ * GET - List all tools for a workflow MCP server
+ */
+export const GET = withMcpAuth('read')(
+ async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
+ try {
+ const { id: serverId } = await params
+
+ logger.info(`[${requestId}] Listing tools for workflow MCP server: ${serverId}`)
+
+ // Verify server exists and belongs to workspace
+ const [server] = await db
+ .select({ id: workflowMcpServer.id })
+ .from(workflowMcpServer)
+ .where(
+ and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
+ )
+ .limit(1)
+
+ if (!server) {
+ return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
+ }
+
+ // Get tools with workflow details
+ const tools = await db
+ .select({
+ id: workflowMcpTool.id,
+ serverId: workflowMcpTool.serverId,
+ workflowId: workflowMcpTool.workflowId,
+ toolName: workflowMcpTool.toolName,
+ toolDescription: workflowMcpTool.toolDescription,
+ parameterSchema: workflowMcpTool.parameterSchema,
+ createdAt: workflowMcpTool.createdAt,
+ updatedAt: workflowMcpTool.updatedAt,
+ workflowName: workflow.name,
+ workflowDescription: workflow.description,
+ isDeployed: workflow.isDeployed,
+ })
+ .from(workflowMcpTool)
+ .leftJoin(workflow, eq(workflowMcpTool.workflowId, workflow.id))
+ .where(eq(workflowMcpTool.serverId, serverId))
+
+ logger.info(`[${requestId}] Found ${tools.length} tools for server ${serverId}`)
+
+ return createMcpSuccessResponse({ tools })
+ } catch (error) {
+ logger.error(`[${requestId}] Error listing tools:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to list tools'),
+ 'Failed to list tools',
+ 500
+ )
+ }
+ }
+)
+
+/**
+ * POST - Add a workflow as a tool to an MCP server
+ */
+export const POST = withMcpAuth('write')(
+ async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
+ try {
+ const { id: serverId } = await params
+ const body = getParsedBody(request) || (await request.json())
+
+ logger.info(`[${requestId}] Adding tool to workflow MCP server: ${serverId}`, {
+ workflowId: body.workflowId,
+ })
+
+ if (!body.workflowId) {
+ return createMcpErrorResponse(
+ new Error('Missing required field: workflowId'),
+ 'Missing required field',
+ 400
+ )
+ }
+
+ // Verify server exists and belongs to workspace
+ const [server] = await db
+ .select({ id: workflowMcpServer.id })
+ .from(workflowMcpServer)
+ .where(
+ and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
+ )
+ .limit(1)
+
+ if (!server) {
+ return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
+ }
+
+ // Verify workflow exists and is deployed
+ const [workflowRecord] = await db
+ .select({
+ id: workflow.id,
+ name: workflow.name,
+ description: workflow.description,
+ isDeployed: workflow.isDeployed,
+ workspaceId: workflow.workspaceId,
+ })
+ .from(workflow)
+ .where(eq(workflow.id, body.workflowId))
+ .limit(1)
+
+ if (!workflowRecord) {
+ return createMcpErrorResponse(new Error('Workflow not found'), 'Workflow not found', 404)
+ }
+
+ // Verify workflow belongs to the same workspace
+ if (workflowRecord.workspaceId !== workspaceId) {
+ return createMcpErrorResponse(
+ new Error('Workflow does not belong to this workspace'),
+ 'Access denied',
+ 403
+ )
+ }
+
+ if (!workflowRecord.isDeployed) {
+ return createMcpErrorResponse(
+ new Error('Workflow must be deployed before adding as a tool'),
+ 'Workflow not deployed',
+ 400
+ )
+ }
+
+ // Verify workflow has a valid start block
+ const hasStartBlock = await hasValidStartBlock(body.workflowId)
+ if (!hasStartBlock) {
+ return createMcpErrorResponse(
+ new Error('Workflow must have a Start block to be used as an MCP tool'),
+ 'No start block found',
+ 400
+ )
+ }
+
+ // Check if tool already exists for this workflow
+ const [existingTool] = await db
+ .select({ id: workflowMcpTool.id })
+ .from(workflowMcpTool)
+ .where(
+ and(
+ eq(workflowMcpTool.serverId, serverId),
+ eq(workflowMcpTool.workflowId, body.workflowId)
+ )
+ )
+ .limit(1)
+
+ if (existingTool) {
+ return createMcpErrorResponse(
+ new Error('This workflow is already added as a tool to this server'),
+ 'Tool already exists',
+ 409
+ )
+ }
+
+ const toolName = sanitizeToolName(body.toolName?.trim() || workflowRecord.name)
+ const toolDescription =
+ body.toolDescription?.trim() ||
+ workflowRecord.description ||
+ `Execute ${workflowRecord.name} workflow`
+
+ // Create the tool
+ const toolId = crypto.randomUUID()
+ const [tool] = await db
+ .insert(workflowMcpTool)
+ .values({
+ id: toolId,
+ serverId,
+ workflowId: body.workflowId,
+ toolName,
+ toolDescription,
+ parameterSchema: body.parameterSchema || {},
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .returning()
+
+ logger.info(
+ `[${requestId}] Successfully added tool ${toolName} (workflow: ${body.workflowId}) to server ${serverId}`
+ )
+
+ return createMcpSuccessResponse({ tool }, 201)
+ } catch (error) {
+ logger.error(`[${requestId}] Error adding tool:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to add tool'),
+ 'Failed to add tool',
+ 500
+ )
+ }
+ }
+)
diff --git a/apps/sim/app/api/mcp/workflow-servers/route.ts b/apps/sim/app/api/mcp/workflow-servers/route.ts
new file mode 100644
index 0000000000..25258e0b21
--- /dev/null
+++ b/apps/sim/app/api/mcp/workflow-servers/route.ts
@@ -0,0 +1,132 @@
+import { db } from '@sim/db'
+import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
+import { createLogger } from '@sim/logger'
+import { eq, inArray, sql } from 'drizzle-orm'
+import type { NextRequest } from 'next/server'
+import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
+import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
+
+const logger = createLogger('WorkflowMcpServersAPI')
+
+export const dynamic = 'force-dynamic'
+
+/**
+ * GET - List all workflow MCP servers for the workspace
+ */
+export const GET = withMcpAuth('read')(
+ async (request: NextRequest, { userId, workspaceId, requestId }) => {
+ try {
+ logger.info(`[${requestId}] Listing workflow MCP servers for workspace ${workspaceId}`)
+
+ const servers = await db
+ .select({
+ id: workflowMcpServer.id,
+ workspaceId: workflowMcpServer.workspaceId,
+ createdBy: workflowMcpServer.createdBy,
+ name: workflowMcpServer.name,
+ description: workflowMcpServer.description,
+ createdAt: workflowMcpServer.createdAt,
+ updatedAt: workflowMcpServer.updatedAt,
+ toolCount: sql`(
+ SELECT COUNT(*)::int
+ FROM "workflow_mcp_tool"
+ WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
+ )`.as('tool_count'),
+ })
+ .from(workflowMcpServer)
+ .where(eq(workflowMcpServer.workspaceId, workspaceId))
+
+ // Fetch all tools for these servers
+ const serverIds = servers.map((s) => s.id)
+ const tools =
+ serverIds.length > 0
+ ? await db
+ .select({
+ serverId: workflowMcpTool.serverId,
+ toolName: workflowMcpTool.toolName,
+ })
+ .from(workflowMcpTool)
+ .where(inArray(workflowMcpTool.serverId, serverIds))
+ : []
+
+ // Group tool names by server
+ const toolNamesByServer: Record = {}
+ for (const tool of tools) {
+ if (!toolNamesByServer[tool.serverId]) {
+ toolNamesByServer[tool.serverId] = []
+ }
+ toolNamesByServer[tool.serverId].push(tool.toolName)
+ }
+
+ // Attach tool names to servers
+ const serversWithToolNames = servers.map((server) => ({
+ ...server,
+ toolNames: toolNamesByServer[server.id] || [],
+ }))
+
+ logger.info(
+ `[${requestId}] Listed ${servers.length} workflow MCP servers for workspace ${workspaceId}`
+ )
+ return createMcpSuccessResponse({ servers: serversWithToolNames })
+ } catch (error) {
+ logger.error(`[${requestId}] Error listing workflow MCP servers:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to list workflow MCP servers'),
+ 'Failed to list workflow MCP servers',
+ 500
+ )
+ }
+ }
+)
+
+/**
+ * POST - Create a new workflow MCP server
+ */
+export const POST = withMcpAuth('write')(
+ async (request: NextRequest, { userId, workspaceId, requestId }) => {
+ try {
+ const body = getParsedBody(request) || (await request.json())
+
+ logger.info(`[${requestId}] Creating workflow MCP server:`, {
+ name: body.name,
+ workspaceId,
+ })
+
+ if (!body.name) {
+ return createMcpErrorResponse(
+ new Error('Missing required field: name'),
+ 'Missing required field',
+ 400
+ )
+ }
+
+ const serverId = crypto.randomUUID()
+
+ const [server] = await db
+ .insert(workflowMcpServer)
+ .values({
+ id: serverId,
+ workspaceId,
+ createdBy: userId,
+ name: body.name.trim(),
+ description: body.description?.trim() || null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .returning()
+
+ logger.info(
+ `[${requestId}] Successfully created workflow MCP server: ${body.name} (ID: ${serverId})`
+ )
+
+ return createMcpSuccessResponse({ server }, 201)
+ } catch (error) {
+ logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
+ return createMcpErrorResponse(
+ error instanceof Error ? error : new Error('Failed to create workflow MCP server'),
+ 'Failed to create workflow MCP server',
+ 500
+ )
+ }
+ }
+)
diff --git a/apps/sim/app/api/templates/[id]/route.ts b/apps/sim/app/api/templates/[id]/route.ts
index 6feef0f32f..5e1e4e8c94 100644
--- a/apps/sim/app/api/templates/[id]/route.ts
+++ b/apps/sim/app/api/templates/[id]/route.ts
@@ -70,7 +70,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
.update(templates)
.set({
views: sql`${templates.views} + 1`,
- updatedAt: new Date(),
})
.where(eq(templates.id, id))
diff --git a/apps/sim/app/api/templates/[id]/star/route.ts b/apps/sim/app/api/templates/[id]/star/route.ts
index d7e23c9d45..8f9fc19a0a 100644
--- a/apps/sim/app/api/templates/[id]/star/route.ts
+++ b/apps/sim/app/api/templates/[id]/star/route.ts
@@ -100,7 +100,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
.update(templates)
.set({
stars: sql`${templates.stars} + 1`,
- updatedAt: new Date(),
})
.where(eq(templates.id, id))
})
@@ -160,7 +159,6 @@ export async function DELETE(
.update(templates)
.set({
stars: sql`GREATEST(${templates.stars} - 1, 0)`,
- updatedAt: new Date(),
})
.where(eq(templates.id, id))
})
diff --git a/apps/sim/app/api/templates/[id]/use/route.ts b/apps/sim/app/api/templates/[id]/use/route.ts
index 3ffb9f5b27..26ab63a65a 100644
--- a/apps/sim/app/api/templates/[id]/use/route.ts
+++ b/apps/sim/app/api/templates/[id]/use/route.ts
@@ -136,7 +136,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
// Prepare template update data
const updateData: any = {
views: sql`${templates.views} + 1`,
- updatedAt: now,
}
// If connecting to template for editing, also update the workflowId
diff --git a/apps/sim/app/api/tools/jsm/approvals/route.ts b/apps/sim/app/api/tools/jsm/approvals/route.ts
new file mode 100644
index 0000000000..896e5bab9d
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/approvals/route.ts
@@ -0,0 +1,183 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import {
+ validateAlphanumericId,
+ validateEnum,
+ validateJiraCloudId,
+ validateJiraIssueKey,
+} from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmApprovalsAPI')
+
+const VALID_ACTIONS = ['get', 'answer'] as const
+const VALID_DECISIONS = ['approve', 'decline'] as const
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const {
+ domain,
+ accessToken,
+ cloudId: cloudIdParam,
+ action,
+ issueIdOrKey,
+ approvalId,
+ decision,
+ start,
+ limit,
+ } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!issueIdOrKey) {
+ logger.error('Missing issueIdOrKey in request')
+ return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
+ }
+
+ if (!action) {
+ logger.error('Missing action in request')
+ return NextResponse.json({ error: 'Action is required' }, { status: 400 })
+ }
+
+ const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
+ if (!actionValidation.isValid) {
+ return NextResponse.json({ error: actionValidation.error }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
+ if (!issueIdOrKeyValidation.isValid) {
+ return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ if (action === 'get') {
+ const params = new URLSearchParams()
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/approval${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching approvals from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ approvals: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ }
+ if (action === 'answer') {
+ if (!approvalId) {
+ logger.error('Missing approvalId in request')
+ return NextResponse.json({ error: 'Approval ID is required' }, { status: 400 })
+ }
+
+ const approvalIdValidation = validateAlphanumericId(approvalId, 'approvalId')
+ if (!approvalIdValidation.isValid) {
+ return NextResponse.json({ error: approvalIdValidation.error }, { status: 400 })
+ }
+
+ const decisionValidation = validateEnum(decision, VALID_DECISIONS, 'decision')
+ if (!decisionValidation.isValid) {
+ return NextResponse.json({ error: decisionValidation.error }, { status: 400 })
+ }
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/approval/${approvalId}`
+
+ logger.info('Answering approval:', { issueIdOrKey, approvalId, decision })
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getJsmHeaders(accessToken),
+ body: JSON.stringify({ decision }),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ approvalId,
+ decision,
+ success: true,
+ approval: data,
+ },
+ })
+ }
+
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
+ } catch (error) {
+ logger.error('Error in approvals operation:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/comment/route.ts b/apps/sim/app/api/tools/jsm/comment/route.ts
new file mode 100644
index 0000000000..b93b90ecce
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/comment/route.ts
@@ -0,0 +1,109 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmCommentAPI')
+
+export async function POST(request: Request) {
+ try {
+ const {
+ domain,
+ accessToken,
+ cloudId: providedCloudId,
+ issueIdOrKey,
+ body: commentBody,
+ isPublic,
+ } = await request.json()
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!issueIdOrKey) {
+ logger.error('Missing issueIdOrKey in request')
+ return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
+ }
+
+ if (!commentBody) {
+ logger.error('Missing comment body in request')
+ return NextResponse.json({ error: 'Comment body is required' }, { status: 400 })
+ }
+
+ const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
+ if (!issueIdOrKeyValidation.isValid) {
+ return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/comment`
+
+ logger.info('Adding comment to:', url)
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getJsmHeaders(accessToken),
+ body: JSON.stringify({
+ body: commentBody,
+ public: isPublic ?? true,
+ }),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ commentId: data.id,
+ body: data.body,
+ isPublic: data.public,
+ success: true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error adding comment:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/comments/route.ts b/apps/sim/app/api/tools/jsm/comments/route.ts
new file mode 100644
index 0000000000..429857f7c2
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/comments/route.ts
@@ -0,0 +1,108 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmCommentsAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const {
+ domain,
+ accessToken,
+ cloudId: cloudIdParam,
+ issueIdOrKey,
+ isPublic,
+ internal,
+ start,
+ limit,
+ } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!issueIdOrKey) {
+ logger.error('Missing issueIdOrKey in request')
+ return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
+ if (!issueIdOrKeyValidation.isValid) {
+ return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const params = new URLSearchParams()
+ if (isPublic) params.append('public', isPublic)
+ if (internal) params.append('internal', internal)
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/comment${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching comments from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ comments: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error fetching comments:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/customers/route.ts b/apps/sim/app/api/tools/jsm/customers/route.ts
new file mode 100644
index 0000000000..de735337dc
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/customers/route.ts
@@ -0,0 +1,155 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmCustomersAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const {
+ domain,
+ accessToken,
+ cloudId: cloudIdParam,
+ serviceDeskId,
+ query,
+ start,
+ limit,
+ emails,
+ } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!serviceDeskId) {
+ logger.error('Missing serviceDeskId in request')
+ return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
+ if (!serviceDeskIdValidation.isValid) {
+ return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const parsedEmails = emails
+ ? typeof emails === 'string'
+ ? emails
+ .split(',')
+ .map((email: string) => email.trim())
+ .filter((email: string) => email)
+ : emails
+ : []
+
+ const isAddOperation = parsedEmails.length > 0
+
+ if (isAddOperation) {
+ const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer`
+
+ logger.info('Adding customers to:', url, { emails: parsedEmails })
+
+ const requestBody: Record = {
+ usernames: parsedEmails,
+ }
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getJsmHeaders(accessToken),
+ body: JSON.stringify(requestBody),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ serviceDeskId,
+ success: true,
+ },
+ })
+ }
+ const params = new URLSearchParams()
+ if (query) params.append('query', query)
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching customers from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ customers: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error with customers operation:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/organization/route.ts b/apps/sim/app/api/tools/jsm/organization/route.ts
new file mode 100644
index 0000000000..168a76be8a
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/organization/route.ts
@@ -0,0 +1,171 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import {
+ validateAlphanumericId,
+ validateEnum,
+ validateJiraCloudId,
+} from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmOrganizationAPI')
+
+const VALID_ACTIONS = ['create', 'add_to_service_desk'] as const
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const {
+ domain,
+ accessToken,
+ cloudId: cloudIdParam,
+ action,
+ name,
+ serviceDeskId,
+ organizationId,
+ } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!action) {
+ logger.error('Missing action in request')
+ return NextResponse.json({ error: 'Action is required' }, { status: 400 })
+ }
+
+ const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
+ if (!actionValidation.isValid) {
+ return NextResponse.json({ error: actionValidation.error }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ if (action === 'create') {
+ if (!name) {
+ logger.error('Missing organization name in request')
+ return NextResponse.json({ error: 'Organization name is required' }, { status: 400 })
+ }
+
+ const url = `${baseUrl}/organization`
+
+ logger.info('Creating organization:', { name })
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getJsmHeaders(accessToken),
+ body: JSON.stringify({ name }),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ organizationId: data.id,
+ name: data.name,
+ success: true,
+ },
+ })
+ }
+ if (action === 'add_to_service_desk') {
+ if (!serviceDeskId) {
+ logger.error('Missing serviceDeskId in request')
+ return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
+ }
+
+ if (!organizationId) {
+ logger.error('Missing organizationId in request')
+ return NextResponse.json({ error: 'Organization ID is required' }, { status: 400 })
+ }
+
+ const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
+ if (!serviceDeskIdValidation.isValid) {
+ return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
+ }
+
+ const organizationIdValidation = validateAlphanumericId(organizationId, 'organizationId')
+ if (!organizationIdValidation.isValid) {
+ return NextResponse.json({ error: organizationIdValidation.error }, { status: 400 })
+ }
+
+ const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization`
+
+ logger.info('Adding organization to service desk:', { serviceDeskId, organizationId })
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getJsmHeaders(accessToken),
+ body: JSON.stringify({ organizationId: Number.parseInt(organizationId, 10) }),
+ })
+
+ if (response.status === 204 || response.ok) {
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ serviceDeskId,
+ organizationId,
+ success: true,
+ },
+ })
+ }
+
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
+ } catch (error) {
+ logger.error('Error in organization operation:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/organizations/route.ts b/apps/sim/app/api/tools/jsm/organizations/route.ts
new file mode 100644
index 0000000000..b6b0f04001
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/organizations/route.ts
@@ -0,0 +1,96 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmOrganizationsAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!serviceDeskId) {
+ logger.error('Missing serviceDeskId in request')
+ return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
+ if (!serviceDeskIdValidation.isValid) {
+ return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const params = new URLSearchParams()
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching organizations from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ organizations: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error fetching organizations:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/participants/route.ts b/apps/sim/app/api/tools/jsm/participants/route.ts
new file mode 100644
index 0000000000..649edd29b7
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/participants/route.ts
@@ -0,0 +1,176 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import {
+ validateEnum,
+ validateJiraCloudId,
+ validateJiraIssueKey,
+} from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmParticipantsAPI')
+
+const VALID_ACTIONS = ['get', 'add'] as const
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const {
+ domain,
+ accessToken,
+ cloudId: cloudIdParam,
+ action,
+ issueIdOrKey,
+ accountIds,
+ start,
+ limit,
+ } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!issueIdOrKey) {
+ logger.error('Missing issueIdOrKey in request')
+ return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
+ }
+
+ if (!action) {
+ logger.error('Missing action in request')
+ return NextResponse.json({ error: 'Action is required' }, { status: 400 })
+ }
+
+ const actionValidation = validateEnum(action, VALID_ACTIONS, 'action')
+ if (!actionValidation.isValid) {
+ return NextResponse.json({ error: actionValidation.error }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
+ if (!issueIdOrKeyValidation.isValid) {
+ return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ if (action === 'get') {
+ const params = new URLSearchParams()
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/participant${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching participants from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ participants: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ }
+ if (action === 'add') {
+ if (!accountIds) {
+ logger.error('Missing accountIds in request')
+ return NextResponse.json({ error: 'Account IDs are required' }, { status: 400 })
+ }
+
+ const parsedAccountIds =
+ typeof accountIds === 'string'
+ ? accountIds
+ .split(',')
+ .map((id: string) => id.trim())
+ .filter((id: string) => id)
+ : accountIds
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/participant`
+
+ logger.info('Adding participants to:', url, { accountIds: parsedAccountIds })
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getJsmHeaders(accessToken),
+ body: JSON.stringify({ accountIds: parsedAccountIds }),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ participants: data.values || [],
+ success: true,
+ },
+ })
+ }
+
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
+ } catch (error) {
+ logger.error('Error in participants operation:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/queues/route.ts b/apps/sim/app/api/tools/jsm/queues/route.ts
new file mode 100644
index 0000000000..691de5d9cf
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/queues/route.ts
@@ -0,0 +1,105 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmQueuesAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const {
+ domain,
+ accessToken,
+ cloudId: cloudIdParam,
+ serviceDeskId,
+ includeCount,
+ start,
+ limit,
+ } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!serviceDeskId) {
+ logger.error('Missing serviceDeskId in request')
+ return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
+ if (!serviceDeskIdValidation.isValid) {
+ return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const params = new URLSearchParams()
+ if (includeCount) params.append('includeCount', includeCount)
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/servicedesk/${serviceDeskId}/queue${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching queues from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ queues: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error fetching queues:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/request/route.ts b/apps/sim/app/api/tools/jsm/request/route.ts
new file mode 100644
index 0000000000..86f18b5bea
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/request/route.ts
@@ -0,0 +1,169 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import {
+ validateAlphanumericId,
+ validateJiraCloudId,
+ validateJiraIssueKey,
+} from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmRequestAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const {
+ domain,
+ accessToken,
+ cloudId: cloudIdParam,
+ issueIdOrKey,
+ serviceDeskId,
+ requestTypeId,
+ summary,
+ description,
+ raiseOnBehalfOf,
+ requestFieldValues,
+ } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const isCreateOperation = serviceDeskId && requestTypeId && summary
+
+ if (isCreateOperation) {
+ const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
+ if (!serviceDeskIdValidation.isValid) {
+ return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
+ }
+
+ const requestTypeIdValidation = validateAlphanumericId(requestTypeId, 'requestTypeId')
+ if (!requestTypeIdValidation.isValid) {
+ return NextResponse.json({ error: requestTypeIdValidation.error }, { status: 400 })
+ }
+ const url = `${baseUrl}/request`
+
+ logger.info('Creating request at:', url)
+
+ const requestBody: Record = {
+ serviceDeskId,
+ requestTypeId,
+ requestFieldValues: requestFieldValues || {
+ summary,
+ ...(description && { description }),
+ },
+ }
+
+ if (raiseOnBehalfOf) {
+ requestBody.raiseOnBehalfOf = raiseOnBehalfOf
+ }
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getJsmHeaders(accessToken),
+ body: JSON.stringify(requestBody),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueId: data.issueId,
+ issueKey: data.issueKey,
+ requestTypeId: data.requestTypeId,
+ serviceDeskId: data.serviceDeskId,
+ success: true,
+ url: `https://${domain}/browse/${data.issueKey}`,
+ },
+ })
+ }
+ if (!issueIdOrKey) {
+ logger.error('Missing issueIdOrKey in request')
+ return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
+ }
+
+ const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
+ if (!issueIdOrKeyValidation.isValid) {
+ return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
+ }
+
+ const url = `${baseUrl}/request/${issueIdOrKey}`
+
+ logger.info('Fetching request from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ request: data,
+ },
+ })
+ } catch (error) {
+ logger.error('Error with request operation:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/requests/route.ts b/apps/sim/app/api/tools/jsm/requests/route.ts
new file mode 100644
index 0000000000..e12ea98fe0
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/requests/route.ts
@@ -0,0 +1,111 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmRequestsAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const {
+ domain,
+ accessToken,
+ cloudId: cloudIdParam,
+ serviceDeskId,
+ requestOwnership,
+ requestStatus,
+ searchTerm,
+ start,
+ limit,
+ } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ if (serviceDeskId) {
+ const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
+ if (!serviceDeskIdValidation.isValid) {
+ return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
+ }
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const params = new URLSearchParams()
+ if (serviceDeskId) params.append('serviceDeskId', serviceDeskId)
+ if (requestOwnership && requestOwnership !== 'ALL_REQUESTS') {
+ params.append('requestOwnership', requestOwnership)
+ }
+ if (requestStatus && requestStatus !== 'ALL') {
+ params.append('requestStatus', requestStatus)
+ }
+ if (searchTerm) params.append('searchTerm', searchTerm)
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/request${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching requests from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ requests: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error fetching requests:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/requesttypes/route.ts b/apps/sim/app/api/tools/jsm/requesttypes/route.ts
new file mode 100644
index 0000000000..d7e9bdb282
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/requesttypes/route.ts
@@ -0,0 +1,96 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmRequestTypesAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!serviceDeskId) {
+ logger.error('Missing serviceDeskId in request')
+ return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
+ if (!serviceDeskIdValidation.isValid) {
+ return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const params = new URLSearchParams()
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching request types from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ requestTypes: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error fetching request types:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/servicedesks/route.ts b/apps/sim/app/api/tools/jsm/servicedesks/route.ts
new file mode 100644
index 0000000000..bab91d5bb7
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/servicedesks/route.ts
@@ -0,0 +1,86 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateJiraCloudId } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmServiceDesksAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const { domain, accessToken, cloudId: cloudIdParam, start, limit } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const params = new URLSearchParams()
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/servicedesk${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching service desks from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ serviceDesks: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error fetching service desks:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/sla/route.ts b/apps/sim/app/api/tools/jsm/sla/route.ts
new file mode 100644
index 0000000000..ac826afcfc
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/sla/route.ts
@@ -0,0 +1,97 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmSlaAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, start, limit } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!issueIdOrKey) {
+ logger.error('Missing issueIdOrKey in request')
+ return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
+ if (!issueIdOrKeyValidation.isValid) {
+ return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const params = new URLSearchParams()
+ if (start) params.append('start', start)
+ if (limit) params.append('limit', limit)
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/sla${params.toString() ? `?${params.toString()}` : ''}`
+
+ logger.info('Fetching SLA info from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ slas: data.values || [],
+ total: data.size || 0,
+ isLastPage: data.isLastPage ?? true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error fetching SLA info:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/transition/route.ts b/apps/sim/app/api/tools/jsm/transition/route.ts
new file mode 100644
index 0000000000..5d17a7ca42
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/transition/route.ts
@@ -0,0 +1,121 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import {
+ validateAlphanumericId,
+ validateJiraCloudId,
+ validateJiraIssueKey,
+} from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmTransitionAPI')
+
+export async function POST(request: Request) {
+ try {
+ const {
+ domain,
+ accessToken,
+ cloudId: providedCloudId,
+ issueIdOrKey,
+ transitionId,
+ comment,
+ } = await request.json()
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!issueIdOrKey) {
+ logger.error('Missing issueIdOrKey in request')
+ return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
+ }
+
+ if (!transitionId) {
+ logger.error('Missing transitionId in request')
+ return NextResponse.json({ error: 'Transition ID is required' }, { status: 400 })
+ }
+
+ const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
+ if (!issueIdOrKeyValidation.isValid) {
+ return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
+ }
+
+ const transitionIdValidation = validateAlphanumericId(transitionId, 'transitionId')
+ if (!transitionIdValidation.isValid) {
+ return NextResponse.json({ error: transitionIdValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/transition`
+
+ logger.info('Transitioning request at:', url)
+
+ const body: Record = {
+ id: transitionId,
+ }
+
+ if (comment) {
+ body.additionalComment = {
+ body: comment,
+ }
+ }
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getJsmHeaders(accessToken),
+ body: JSON.stringify(body),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ transitionId,
+ success: true,
+ },
+ })
+ } catch (error) {
+ logger.error('Error transitioning request:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/tools/jsm/transitions/route.ts b/apps/sim/app/api/tools/jsm/transitions/route.ts
new file mode 100644
index 0000000000..a9242177bf
--- /dev/null
+++ b/apps/sim/app/api/tools/jsm/transitions/route.ts
@@ -0,0 +1,91 @@
+import { createLogger } from '@sim/logger'
+import { NextResponse } from 'next/server'
+import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
+import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('JsmTransitionsAPI')
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json()
+ const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body
+
+ if (!domain) {
+ logger.error('Missing domain in request')
+ return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
+ }
+
+ if (!accessToken) {
+ logger.error('Missing access token in request')
+ return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
+ }
+
+ if (!issueIdOrKey) {
+ logger.error('Missing issueIdOrKey in request')
+ return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
+ }
+
+ const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
+
+ const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
+ if (!cloudIdValidation.isValid) {
+ return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
+ }
+
+ const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
+ if (!issueIdOrKeyValidation.isValid) {
+ return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
+ }
+
+ const baseUrl = getJsmApiBaseUrl(cloudId)
+
+ const url = `${baseUrl}/request/${issueIdOrKey}/transition`
+
+ logger.info('Fetching transitions from:', url)
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getJsmHeaders(accessToken),
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ logger.error('JSM API error:', {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText,
+ })
+
+ return NextResponse.json(
+ { error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
+ { status: response.status }
+ )
+ }
+
+ const data = await response.json()
+
+ return NextResponse.json({
+ success: true,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey,
+ transitions: data.values || [],
+ },
+ })
+ } catch (error) {
+ logger.error('Error fetching transitions:', {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined,
+ })
+
+ return NextResponse.json(
+ {
+ error: error instanceof Error ? error.message : 'Internal server error',
+ success: false,
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts
index c54124f47d..2413eaa2db 100644
--- a/apps/sim/app/api/workflows/[id]/deploy/route.ts
+++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import { and, desc, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { generateRequestId } from '@/lib/core/utils/request'
+import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
import { deployWorkflow, loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import {
createSchedulesForDeploy,
@@ -160,6 +161,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
logger.info(`[${requestId}] Workflow deployed successfully: ${id}`)
+ // Sync MCP tools with the latest parameter schema
+ await syncMcpToolsForWorkflow({ workflowId: id, requestId, context: 'deploy' })
+
const responseApiKeyInfo = workflowData!.workspaceId
? 'Workspace API keys'
: 'Personal API keys'
@@ -217,6 +221,9 @@ export async function DELETE(
.where(eq(workflow.id, id))
})
+ // Remove all MCP tools that reference this workflow
+ await removeMcpToolsForWorkflow(id, requestId)
+
logger.info(`[${requestId}] Workflow undeployed successfully: ${id}`)
try {
diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/activate/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/activate/route.ts
index 1ef4761e68..4ffc35f9ef 100644
--- a/apps/sim/app/api/workflows/[id]/deployments/[version]/activate/route.ts
+++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/activate/route.ts
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { generateRequestId } from '@/lib/core/utils/request'
+import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -31,6 +32,18 @@ export async function POST(
const now = new Date()
+ // Get the state of the version being activated for MCP tool sync
+ const [versionData] = await db
+ .select({ state: workflowDeploymentVersion.state })
+ .from(workflowDeploymentVersion)
+ .where(
+ and(
+ eq(workflowDeploymentVersion.workflowId, id),
+ eq(workflowDeploymentVersion.version, versionNum)
+ )
+ )
+ .limit(1)
+
await db.transaction(async (tx) => {
await tx
.update(workflowDeploymentVersion)
@@ -65,6 +78,16 @@ export async function POST(
await tx.update(workflow).set(updateData).where(eq(workflow.id, id))
})
+ // Sync MCP tools with the activated version's parameter schema
+ if (versionData?.state) {
+ await syncMcpToolsForWorkflow({
+ workflowId: id,
+ requestId,
+ state: versionData.state,
+ context: 'activate',
+ })
+ }
+
return createSuccessResponse({ success: true, deployedAt: now })
} catch (error: any) {
logger.error(`[${requestId}] Error activating deployment for workflow: ${id}`, error)
diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
index 5b33e6c146..5e8f43560f 100644
--- a/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
+++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/revert/route.ts
@@ -4,6 +4,7 @@ import { and, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { env } from '@/lib/core/config/env'
import { generateRequestId } from '@/lib/core/utils/request'
+import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -87,6 +88,14 @@ export async function POST(
.set({ lastSynced: new Date(), updatedAt: new Date() })
.where(eq(workflow.id, id))
+ // Sync MCP tools with the reverted version's parameter schema
+ await syncMcpToolsForWorkflow({
+ workflowId: id,
+ requestId,
+ state: deployedState,
+ context: 'revert',
+ })
+
try {
const socketServerUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002'
await fetch(`${socketServerUrl}/api/workflow-reverted`, {
diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts
index 5d1a7d7a02..7368394a6a 100644
--- a/apps/sim/app/api/workflows/[id]/execute/route.ts
+++ b/apps/sim/app/api/workflows/[id]/execute/route.ts
@@ -109,7 +109,7 @@ type AsyncExecutionParams = {
workflowId: string
userId: string
input: any
- triggerType: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
+ triggerType: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
}
/**
@@ -252,14 +252,15 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
})
const executionId = uuidv4()
- type LoggingTriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
+ type LoggingTriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
let loggingTriggerType: LoggingTriggerType = 'manual'
if (
triggerType === 'api' ||
triggerType === 'chat' ||
triggerType === 'webhook' ||
triggerType === 'schedule' ||
- triggerType === 'manual'
+ triggerType === 'manual' ||
+ triggerType === 'mcp'
) {
loggingTriggerType = triggerType as LoggingTriggerType
}
diff --git a/apps/sim/app/changelog/components/timeline-list.tsx b/apps/sim/app/changelog/components/timeline-list.tsx
index 9e2b81a03a..74f65b900d 100644
--- a/apps/sim/app/changelog/components/timeline-list.tsx
+++ b/apps/sim/app/changelog/components/timeline-list.tsx
@@ -2,7 +2,7 @@
import React from 'react'
import ReactMarkdown from 'react-markdown'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import type { ChangelogEntry } from '@/app/changelog/components/changelog-content'
diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx
index da80a06d21..6be1e579c5 100644
--- a/apps/sim/app/layout.tsx
+++ b/apps/sim/app/layout.tsx
@@ -41,7 +41,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
}}
/>
- {/* Workspace layout dimensions: set CSS vars before hydration to avoid layout jump */}
+ {/*
+ Workspace layout dimensions: set CSS vars before hydration to avoid layout jump.
+
+ IMPORTANT: These hardcoded values must stay in sync with stores/constants.ts
+ We cannot use imports here since this is a blocking script that runs before React.
+ */}
')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Only HTTP and HTTPS protocols are allowed')
- })
-
- it.concurrent('rejects ssh protocol', () => {
- const result = validateMcpServerUrl('ssh://user@host.com')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Only HTTP and HTTPS protocols are allowed')
- })
- })
-
- describe('SSRF Protection - Blocked Hostnames', () => {
- it.concurrent('rejects localhost', () => {
- const result = validateMcpServerUrl('https://localhost/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('localhost')
- expect(result.error).toContain('not allowed for security reasons')
- })
-
- it.concurrent('rejects Google Cloud metadata endpoint', () => {
- const result = validateMcpServerUrl('http://metadata.google.internal/computeMetadata/v1/')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('metadata.google.internal')
- })
-
- it.concurrent('rejects Azure metadata endpoint', () => {
- const result = validateMcpServerUrl('http://metadata.azure.com/metadata/instance')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('metadata.azure.com')
- })
-
- it.concurrent('rejects AWS metadata IP', () => {
- const result = validateMcpServerUrl('http://169.254.169.254/latest/meta-data/')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('169.254.169.254')
- })
-
- it.concurrent('rejects consul service discovery', () => {
- const result = validateMcpServerUrl('http://consul/v1/agent/services')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('consul')
- })
-
- it.concurrent('rejects etcd service discovery', () => {
- const result = validateMcpServerUrl('http://etcd/v2/keys/')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('etcd')
- })
- })
-
- describe('SSRF Protection - Private IPv4 Ranges', () => {
- it.concurrent('rejects loopback address 127.0.0.1', () => {
- const result = validateMcpServerUrl('http://127.0.0.1/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects loopback address 127.0.0.100', () => {
- const result = validateMcpServerUrl('http://127.0.0.100/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects private class A (10.x.x.x)', () => {
- const result = validateMcpServerUrl('http://10.0.0.1/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects private class A (10.255.255.255)', () => {
- const result = validateMcpServerUrl('http://10.255.255.255/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects private class B (172.16.x.x)', () => {
- const result = validateMcpServerUrl('http://172.16.0.1/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects private class B (172.31.255.255)', () => {
- const result = validateMcpServerUrl('http://172.31.255.255/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects private class C (192.168.x.x)', () => {
- const result = validateMcpServerUrl('http://192.168.0.1/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects private class C (192.168.255.255)', () => {
- const result = validateMcpServerUrl('http://192.168.255.255/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects link-local address (169.254.x.x)', () => {
- const result = validateMcpServerUrl('http://169.254.1.1/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('rejects invalid zero range (0.x.x.x)', () => {
- const result = validateMcpServerUrl('http://0.0.0.0/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('Private IP addresses are not allowed')
- })
-
- it.concurrent('accepts valid public IP', () => {
- const result = validateMcpServerUrl('http://8.8.8.8/mcp')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('accepts public IP in non-private range', () => {
- const result = validateMcpServerUrl('http://203.0.113.50/mcp')
- expect(result.isValid).toBe(true)
- })
- })
-
- /**
- * Note: IPv6 private range validation has a known issue where the brackets
- * are not stripped before testing against private ranges. The isIPv6 function
- * strips brackets, but the range test still uses the original bracketed hostname.
- * These tests document the current (buggy) behavior rather than expected behavior.
- */
- describe('SSRF Protection - Private IPv6 Ranges', () => {
- it.concurrent('identifies IPv6 addresses (isIPv6 works correctly)', () => {
- // The validator correctly identifies these as IPv6 addresses
- // but fails to block them due to bracket handling issue
- const result = validateMcpServerUrl('http://[::1]/mcp')
- // Current behavior: passes validation (should ideally be blocked)
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('handles IPv4-mapped IPv6 addresses', () => {
- const result = validateMcpServerUrl('http://[::ffff:192.168.1.1]/mcp')
- // Current behavior: passes validation
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('handles unique local addresses', () => {
- const result = validateMcpServerUrl('http://[fc00::1]/mcp')
- // Current behavior: passes validation
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('handles link-local IPv6 addresses', () => {
- const result = validateMcpServerUrl('http://[fe80::1]/mcp')
- // Current behavior: passes validation
- expect(result.isValid).toBe(true)
- })
- })
-
- describe('SSRF Protection - Blocked Ports', () => {
- it.concurrent('rejects SSH port (22)', () => {
- const result = validateMcpServerUrl('https://api.example.com:22/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 22 is not allowed for security reasons')
- })
-
- it.concurrent('rejects Telnet port (23)', () => {
- const result = validateMcpServerUrl('https://api.example.com:23/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 23 is not allowed for security reasons')
- })
-
- it.concurrent('rejects SMTP port (25)', () => {
- const result = validateMcpServerUrl('https://api.example.com:25/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 25 is not allowed for security reasons')
- })
-
- it.concurrent('rejects DNS port (53)', () => {
- const result = validateMcpServerUrl('https://api.example.com:53/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 53 is not allowed for security reasons')
- })
-
- it.concurrent('rejects MySQL port (3306)', () => {
- const result = validateMcpServerUrl('https://api.example.com:3306/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 3306 is not allowed for security reasons')
- })
-
- it.concurrent('rejects PostgreSQL port (5432)', () => {
- const result = validateMcpServerUrl('https://api.example.com:5432/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 5432 is not allowed for security reasons')
- })
-
- it.concurrent('rejects Redis port (6379)', () => {
- const result = validateMcpServerUrl('https://api.example.com:6379/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 6379 is not allowed for security reasons')
- })
-
- it.concurrent('rejects MongoDB port (27017)', () => {
- const result = validateMcpServerUrl('https://api.example.com:27017/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 27017 is not allowed for security reasons')
- })
-
- it.concurrent('rejects Elasticsearch port (9200)', () => {
- const result = validateMcpServerUrl('https://api.example.com:9200/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('Port 9200 is not allowed for security reasons')
- })
-
- it.concurrent('accepts common web ports (8080)', () => {
- const result = validateMcpServerUrl('https://api.example.com:8080/mcp')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('accepts common web ports (3000)', () => {
- const result = validateMcpServerUrl('https://api.example.com:3000/mcp')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('accepts default HTTPS port (443)', () => {
- const result = validateMcpServerUrl('https://api.example.com:443/mcp')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('accepts default HTTP port (80)', () => {
- const result = validateMcpServerUrl('http://api.example.com:80/mcp')
- expect(result.isValid).toBe(true)
- })
- })
-
- describe('Protocol-Port Mismatch Detection', () => {
- it.concurrent('rejects HTTPS on port 80', () => {
- const result = validateMcpServerUrl('https://api.example.com:80/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('HTTPS URLs should not use port 80')
- })
-
- it.concurrent('rejects HTTP on port 443', () => {
- const result = validateMcpServerUrl('http://api.example.com:443/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('HTTP URLs should not use port 443')
- })
- })
-
- describe('URL Length Validation', () => {
- it.concurrent('accepts URL within length limit', () => {
- const result = validateMcpServerUrl('https://api.example.com/mcp')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('rejects URL exceeding 2048 characters', () => {
- const longPath = 'a'.repeat(2100)
- const result = validateMcpServerUrl(`https://api.example.com/${longPath}`)
- expect(result.isValid).toBe(false)
- expect(result.error).toBe('URL is too long (maximum 2048 characters)')
- })
- })
-
- describe('Edge Cases', () => {
- it.concurrent('handles URL with query parameters', () => {
- const result = validateMcpServerUrl('https://api.example.com/mcp?token=abc123')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('handles URL with fragments', () => {
- const result = validateMcpServerUrl('https://api.example.com/mcp#section')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('handles URL with username:password (basic auth)', () => {
- const result = validateMcpServerUrl('https://user:pass@api.example.com/mcp')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('handles URL with subdomain', () => {
- const result = validateMcpServerUrl('https://mcp.api.example.com/v1')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('handles URL with multiple path segments', () => {
- const result = validateMcpServerUrl('https://api.example.com/v1/mcp/tools')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('is case insensitive for hostname', () => {
- const result = validateMcpServerUrl('https://API.EXAMPLE.COM/mcp')
- expect(result.isValid).toBe(true)
- })
-
- it.concurrent('rejects localhost regardless of case', () => {
- const result = validateMcpServerUrl('https://LOCALHOST/mcp')
- expect(result.isValid).toBe(false)
- expect(result.error).toContain('not allowed for security reasons')
- })
- })
-})
diff --git a/apps/sim/lib/mcp/url-validator.ts b/apps/sim/lib/mcp/url-validator.ts
deleted file mode 100644
index 31694f1317..0000000000
--- a/apps/sim/lib/mcp/url-validator.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * URL Validator for MCP Servers
- *
- * Provides SSRF (Server-Side Request Forgery) protection by validating
- * MCP server URLs against common attack patterns and dangerous destinations.
- */
-
-import { createLogger } from '@sim/logger'
-
-const logger = createLogger('McpUrlValidator')
-
-// Blocked IPv4 ranges
-const PRIVATE_IP_RANGES = [
- /^127\./, // Loopback (127.0.0.0/8)
- /^10\./, // Private class A (10.0.0.0/8)
- /^172\.(1[6-9]|2[0-9]|3[01])\./, // Private class B (172.16.0.0/12)
- /^192\.168\./, // Private class C (192.168.0.0/16)
- /^169\.254\./, // Link-local (169.254.0.0/16)
- /^0\./, // Invalid range
-]
-
-// Blocked IPv6 ranges
-const PRIVATE_IPV6_RANGES = [
- /^::1$/, // Localhost
- /^::ffff:/, // IPv4-mapped IPv6
- /^fc00:/, // Unique local (fc00::/7)
- /^fd00:/, // Unique local (fd00::/8)
- /^fe80:/, // Link-local (fe80::/10)
-]
-
-// Blocked hostnames - SSRF protection
-const BLOCKED_HOSTNAMES = [
- 'localhost',
- // Cloud metadata endpoints
- 'metadata.google.internal', // Google Cloud metadata
- 'metadata.gce.internal', // Google Compute Engine metadata (legacy)
- '169.254.169.254', // AWS/Azure/GCP metadata service IP
- 'metadata.azure.com', // Azure Instance Metadata Service
- 'instance-data.ec2.internal', // AWS EC2 instance metadata (internal)
- // Service discovery endpoints
- 'consul', // HashiCorp Consul
- 'etcd', // etcd key-value store
-]
-
-// Blocked ports
-const BLOCKED_PORTS = [
- 22, // SSH
- 23, // Telnet
- 25, // SMTP
- 53, // DNS
- 110, // POP3
- 143, // IMAP
- 993, // IMAPS
- 995, // POP3S
- 1433, // SQL Server
- 1521, // Oracle
- 3306, // MySQL
- 5432, // PostgreSQL
- 6379, // Redis
- 9200, // Elasticsearch
- 27017, // MongoDB
-]
-
-export interface UrlValidationResult {
- isValid: boolean
- error?: string
- normalizedUrl?: string
-}
-
-export function validateMcpServerUrl(urlString: string): UrlValidationResult {
- if (!urlString || typeof urlString !== 'string') {
- return {
- isValid: false,
- error: 'URL is required and must be a string',
- }
- }
-
- let url: URL
- try {
- url = new URL(urlString.trim())
- } catch (error) {
- return {
- isValid: false,
- error: 'Invalid URL format',
- }
- }
-
- if (!['http:', 'https:'].includes(url.protocol)) {
- return {
- isValid: false,
- error: 'Only HTTP and HTTPS protocols are allowed',
- }
- }
-
- const hostname = url.hostname.toLowerCase()
-
- if (BLOCKED_HOSTNAMES.includes(hostname)) {
- return {
- isValid: false,
- error: `Hostname '${hostname}' is not allowed for security reasons`,
- }
- }
-
- if (isIPv4(hostname)) {
- for (const range of PRIVATE_IP_RANGES) {
- if (range.test(hostname)) {
- return {
- isValid: false,
- error: `Private IP addresses are not allowed: ${hostname}`,
- }
- }
- }
- }
-
- if (isIPv6(hostname)) {
- for (const range of PRIVATE_IPV6_RANGES) {
- if (range.test(hostname)) {
- return {
- isValid: false,
- error: `Private IPv6 addresses are not allowed: ${hostname}`,
- }
- }
- }
- }
-
- if (url.port) {
- const port = Number.parseInt(url.port, 10)
- if (BLOCKED_PORTS.includes(port)) {
- return {
- isValid: false,
- error: `Port ${port} is not allowed for security reasons`,
- }
- }
- }
-
- if (url.toString().length > 2048) {
- return {
- isValid: false,
- error: 'URL is too long (maximum 2048 characters)',
- }
- }
-
- if (url.protocol === 'https:' && url.port === '80') {
- return {
- isValid: false,
- error: 'HTTPS URLs should not use port 80',
- }
- }
-
- if (url.protocol === 'http:' && url.port === '443') {
- return {
- isValid: false,
- error: 'HTTP URLs should not use port 443',
- }
- }
-
- logger.debug(`Validated MCP server URL: ${hostname}`)
-
- return {
- isValid: true,
- normalizedUrl: url.toString(),
- }
-}
-
-function isIPv4(hostname: string): boolean {
- const ipv4Regex =
- /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
- return ipv4Regex.test(hostname)
-}
-
-function isIPv6(hostname: string): boolean {
- const cleanHostname = hostname.replace(/^\[|\]$/g, '')
-
- const ipv6Regex =
- /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::$|^::1$|^(?:[0-9a-fA-F]{1,4}:)*::[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4})*$/
-
- return ipv6Regex.test(cleanHostname)
-}
diff --git a/apps/sim/lib/mcp/utils.ts b/apps/sim/lib/mcp/utils.ts
index 1a446b675e..a732dc3829 100644
--- a/apps/sim/lib/mcp/utils.ts
+++ b/apps/sim/lib/mcp/utils.ts
@@ -7,13 +7,44 @@ import { isMcpTool, MCP } from '@/executor/constants'
*/
export const MCP_CONSTANTS = {
EXECUTION_TIMEOUT: 60000,
- CACHE_TIMEOUT: 5 * 60 * 1000, // 5 minutes
+ CACHE_TIMEOUT: 5 * 60 * 1000,
DEFAULT_RETRIES: 3,
DEFAULT_CONNECTION_TIMEOUT: 30000,
MAX_CACHE_SIZE: 1000,
MAX_CONSECUTIVE_FAILURES: 3,
} as const
+/**
+ * Core MCP tool parameter keys that are metadata, not user-entered test values.
+ * These should be preserved when cleaning up params during schema updates.
+ */
+export const MCP_TOOL_CORE_PARAMS = new Set(['serverId', 'serverUrl', 'toolName', 'serverName'])
+
+/**
+ * Sanitizes a string by removing invisible Unicode characters that cause HTTP header errors.
+ * Handles characters like U+2028 (Line Separator) that can be introduced via copy-paste.
+ */
+export function sanitizeForHttp(value: string): string {
+ return value
+ .replace(/[\u2028\u2029\u200B-\u200D\uFEFF]/g, '')
+ .replace(/[\x00-\x1F\x7F]/g, '')
+ .trim()
+}
+
+/**
+ * Sanitizes all header key-value pairs for HTTP usage.
+ */
+export function sanitizeHeaders(
+ headers: Record | undefined
+): Record | undefined {
+ if (!headers) return headers
+ return Object.fromEntries(
+ Object.entries(headers)
+ .map(([key, value]) => [sanitizeForHttp(key), sanitizeForHttp(value)])
+ .filter(([key, value]) => key !== '' && value !== '')
+ )
+}
+
/**
* Client-safe MCP constants
*/
diff --git a/apps/sim/lib/mcp/workflow-mcp-sync.ts b/apps/sim/lib/mcp/workflow-mcp-sync.ts
new file mode 100644
index 0000000000..c6055a713b
--- /dev/null
+++ b/apps/sim/lib/mcp/workflow-mcp-sync.ts
@@ -0,0 +1,106 @@
+import { db, workflowMcpTool } from '@sim/db'
+import { createLogger } from '@sim/logger'
+import { eq } from 'drizzle-orm'
+import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
+import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
+import { extractInputFormatFromBlocks, generateToolInputSchema } from './workflow-tool-schema'
+
+const logger = createLogger('WorkflowMcpSync')
+
+/**
+ * Generate MCP tool parameter schema from workflow blocks
+ */
+function generateSchemaFromBlocks(blocks: Record): Record {
+ const inputFormat = extractInputFormatFromBlocks(blocks)
+ if (!inputFormat || inputFormat.length === 0) {
+ return { type: 'object', properties: {} }
+ }
+ return generateToolInputSchema(inputFormat) as unknown as Record
+}
+
+interface SyncOptions {
+ workflowId: string
+ requestId: string
+ /** If provided, use this state instead of loading from DB */
+ state?: { blocks?: Record }
+ /** Context for logging (e.g., 'deploy', 'revert', 'activate') */
+ context?: string
+}
+
+/**
+ * Sync MCP tools for a workflow with the latest parameter schema.
+ * - If the workflow has no start block, removes all MCP tools
+ * - Otherwise, updates all MCP tools with the current schema
+ *
+ * @param options.workflowId - The workflow ID to sync
+ * @param options.requestId - Request ID for logging
+ * @param options.state - Optional workflow state (if not provided, loads from DB)
+ * @param options.context - Optional context for log messages
+ */
+export async function syncMcpToolsForWorkflow(options: SyncOptions): Promise {
+ const { workflowId, requestId, state, context = 'sync' } = options
+
+ try {
+ // Get all MCP tools that use this workflow
+ const tools = await db
+ .select({ id: workflowMcpTool.id })
+ .from(workflowMcpTool)
+ .where(eq(workflowMcpTool.workflowId, workflowId))
+
+ if (tools.length === 0) {
+ logger.debug(`[${requestId}] No MCP tools to sync for workflow: ${workflowId}`)
+ return
+ }
+
+ // Get workflow state (from param or load from DB)
+ let workflowState: { blocks?: Record } | null = state ?? null
+ if (!workflowState) {
+ workflowState = await loadWorkflowFromNormalizedTables(workflowId)
+ }
+
+ // Check if workflow has a valid start block
+ if (!hasValidStartBlockInState(workflowState)) {
+ await db.delete(workflowMcpTool).where(eq(workflowMcpTool.workflowId, workflowId))
+ logger.info(
+ `[${requestId}] Removed ${tools.length} MCP tool(s) - workflow has no start block (${context}): ${workflowId}`
+ )
+ return
+ }
+
+ // Generate and update parameter schema
+ const parameterSchema = workflowState?.blocks
+ ? generateSchemaFromBlocks(workflowState.blocks)
+ : { type: 'object', properties: {} }
+
+ await db
+ .update(workflowMcpTool)
+ .set({
+ parameterSchema,
+ updatedAt: new Date(),
+ })
+ .where(eq(workflowMcpTool.workflowId, workflowId))
+
+ logger.info(
+ `[${requestId}] Synced ${tools.length} MCP tool(s) for workflow (${context}): ${workflowId}`
+ )
+ } catch (error) {
+ logger.error(`[${requestId}] Error syncing MCP tools (${context}):`, error)
+ // Don't throw - this is a non-critical operation
+ }
+}
+
+/**
+ * Remove all MCP tools for a workflow (used when undeploying)
+ */
+export async function removeMcpToolsForWorkflow(
+ workflowId: string,
+ requestId: string
+): Promise {
+ try {
+ await db.delete(workflowMcpTool).where(eq(workflowMcpTool.workflowId, workflowId))
+ logger.info(`[${requestId}] Removed MCP tools for workflow: ${workflowId}`)
+ } catch (error) {
+ logger.error(`[${requestId}] Error removing MCP tools:`, error)
+ // Don't throw - this is a non-critical operation
+ }
+}
diff --git a/apps/sim/lib/mcp/workflow-tool-schema.ts b/apps/sim/lib/mcp/workflow-tool-schema.ts
new file mode 100644
index 0000000000..ff915f76bd
--- /dev/null
+++ b/apps/sim/lib/mcp/workflow-tool-schema.ts
@@ -0,0 +1,236 @@
+import { z } from 'zod'
+import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils'
+import { isValidStartBlockType } from '@/lib/workflows/triggers/trigger-utils'
+import type { InputFormatField } from '@/lib/workflows/types'
+import type { McpToolSchema } from './types'
+
+/**
+ * Extended property definition for workflow tool schemas.
+ * More specific than the generic McpToolSchema properties.
+ */
+export interface McpToolProperty {
+ type: string
+ description?: string
+ items?: McpToolProperty
+ properties?: Record
+}
+
+/**
+ * Extended MCP tool schema with typed properties (for workflow tool generation).
+ * Extends the base McpToolSchema with more specific property types.
+ */
+export interface McpToolInputSchema extends McpToolSchema {
+ properties: Record
+}
+
+export interface McpToolDefinition {
+ name: string
+ description: string
+ inputSchema: McpToolInputSchema
+}
+
+/**
+ * File item Zod schema for MCP file inputs.
+ * This is the single source of truth for file structure.
+ */
+export const fileItemZodSchema = z.object({
+ name: z.string().describe('File name'),
+ data: z.string().describe('Base64 encoded file content'),
+ mimeType: z.string().describe('MIME type of the file'),
+})
+
+/**
+ * Convert InputFormatField type to Zod schema
+ */
+function fieldTypeToZod(fieldType: string | undefined, isRequired: boolean): z.ZodTypeAny {
+ let zodType: z.ZodTypeAny
+
+ switch (fieldType) {
+ case 'string':
+ zodType = z.string()
+ break
+ case 'number':
+ zodType = z.number()
+ break
+ case 'boolean':
+ zodType = z.boolean()
+ break
+ case 'object':
+ zodType = z.record(z.any())
+ break
+ case 'array':
+ zodType = z.array(z.any())
+ break
+ case 'files':
+ zodType = z.array(fileItemZodSchema)
+ break
+ default:
+ zodType = z.string()
+ }
+
+ return isRequired ? zodType : zodType.optional()
+}
+
+/**
+ * Generate Zod schema shape from InputFormatField array.
+ * This is used directly by the MCP server for tool registration.
+ */
+export function generateToolZodSchema(inputFormat: InputFormatField[]): z.ZodRawShape | undefined {
+ if (!inputFormat || inputFormat.length === 0) {
+ return undefined
+ }
+
+ const shape: z.ZodRawShape = {}
+
+ for (const field of inputFormat) {
+ if (!field.name) continue
+
+ const zodType = fieldTypeToZod(field.type, true)
+ shape[field.name] = field.name ? zodType.describe(field.name) : zodType
+ }
+
+ return Object.keys(shape).length > 0 ? shape : undefined
+}
+
+/**
+ * Map InputFormatField type to JSON Schema type (for database storage)
+ */
+function mapFieldTypeToJsonSchemaType(fieldType: string | undefined): string {
+ switch (fieldType) {
+ case 'string':
+ return 'string'
+ case 'number':
+ return 'number'
+ case 'boolean':
+ return 'boolean'
+ case 'object':
+ return 'object'
+ case 'array':
+ return 'array'
+ case 'files':
+ return 'array'
+ default:
+ return 'string'
+ }
+}
+
+/**
+ * Sanitize a workflow name to be a valid MCP tool name.
+ * Tool names should be lowercase, alphanumeric with underscores.
+ */
+export function sanitizeToolName(name: string): string {
+ return (
+ name
+ .toLowerCase()
+ .replace(/[^a-z0-9\s_-]/g, '')
+ .replace(/[\s-]+/g, '_')
+ .replace(/_+/g, '_')
+ .replace(/^_|_$/g, '')
+ .substring(0, 64) || 'workflow_tool'
+ )
+}
+
+/**
+ * Generate MCP tool input schema from InputFormatField array.
+ * This converts the workflow's input format definition to JSON Schema format
+ * that MCP clients can use to understand tool parameters.
+ */
+export function generateToolInputSchema(inputFormat: InputFormatField[]): McpToolInputSchema {
+ const properties: Record = {}
+ const required: string[] = []
+
+ for (const field of inputFormat) {
+ if (!field.name) continue
+
+ const fieldName = field.name
+ const fieldType = mapFieldTypeToJsonSchemaType(field.type)
+
+ const property: McpToolProperty = {
+ type: fieldType,
+ // Use custom description if provided, otherwise use field name
+ description: field.description?.trim() || fieldName,
+ }
+
+ // Handle array types
+ if (fieldType === 'array') {
+ if (field.type === 'files') {
+ property.items = {
+ type: 'object',
+ properties: {
+ name: { type: 'string', description: 'File name' },
+ url: { type: 'string', description: 'File URL' },
+ type: { type: 'string', description: 'MIME type' },
+ size: { type: 'number', description: 'File size in bytes' },
+ },
+ }
+ // Use custom description if provided, otherwise use default
+ if (!field.description?.trim()) {
+ property.description = 'Array of file objects'
+ }
+ } else {
+ property.items = { type: 'string' }
+ }
+ }
+
+ properties[fieldName] = property
+
+ // All fields are considered required by default
+ // (in the future, we could add an optional flag to InputFormatField)
+ required.push(fieldName)
+ }
+
+ return {
+ type: 'object',
+ properties,
+ required: required.length > 0 ? required : undefined,
+ }
+}
+
+/**
+ * Generate a complete MCP tool definition from workflow metadata and input format.
+ */
+export function generateToolDefinition(
+ workflowName: string,
+ workflowDescription: string | undefined | null,
+ inputFormat: InputFormatField[],
+ customToolName?: string,
+ customDescription?: string
+): McpToolDefinition {
+ return {
+ name: customToolName || sanitizeToolName(workflowName),
+ description: customDescription || workflowDescription || `Execute ${workflowName} workflow`,
+ inputSchema: generateToolInputSchema(inputFormat),
+ }
+}
+
+/**
+ * Extract input format from a workflow's blocks.
+ * Looks for any valid start block and extracts its inputFormat configuration.
+ */
+export function extractInputFormatFromBlocks(
+ blocks: Record
+): InputFormatField[] | null {
+ // Look for any valid start block
+ for (const [, block] of Object.entries(blocks)) {
+ if (!block || typeof block !== 'object') continue
+
+ const blockObj = block as Record
+ const blockType = blockObj.type as string
+
+ if (isValidStartBlockType(blockType)) {
+ // Try to get inputFormat from subBlocks.inputFormat.value
+ const subBlocks = blockObj.subBlocks as Record | undefined
+ const subBlockValue = subBlocks?.inputFormat?.value
+
+ // Try legacy config.params.inputFormat
+ const config = blockObj.config as Record | undefined
+ const params = config?.params as Record | undefined
+ const paramsValue = params?.inputFormat
+
+ const normalized = normalizeInputFormatValue(subBlockValue ?? paramsValue)
+ return normalized.length > 0 ? normalized : null
+ }
+ }
+
+ return null
+}
diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts
index 9b37265a55..94496a24b0 100644
--- a/apps/sim/lib/oauth/oauth.ts
+++ b/apps/sim/lib/oauth/oauth.ts
@@ -322,7 +322,7 @@ export const OAUTH_PROVIDERS: Record = {
services: {
jira: {
name: 'Jira',
- description: 'Access Jira projects and issues.',
+ description: 'Access Jira projects, issues, and Service Management.',
providerId: 'jira',
icon: JiraIcon,
baseProviderIcon: JiraIcon,
@@ -365,6 +365,35 @@ export const OAUTH_PROVIDERS: Record = {
'read:comment.property:jira',
'read:jql:jira',
'read:field:jira',
+ // Jira Service Management scopes
+ 'read:servicedesk:jira-service-management',
+ 'read:requesttype:jira-service-management',
+ 'read:request:jira-service-management',
+ 'write:request:jira-service-management',
+ 'read:request.comment:jira-service-management',
+ 'write:request.comment:jira-service-management',
+ 'read:customer:jira-service-management',
+ 'write:customer:jira-service-management',
+ 'read:servicedesk.customer:jira-service-management',
+ 'write:servicedesk.customer:jira-service-management',
+ 'read:organization:jira-service-management',
+ 'write:organization:jira-service-management',
+ 'read:servicedesk.organization:jira-service-management',
+ 'write:servicedesk.organization:jira-service-management',
+ 'read:organization.user:jira-service-management',
+ 'write:organization.user:jira-service-management',
+ 'read:organization.property:jira-service-management',
+ 'write:organization.property:jira-service-management',
+ 'read:organization.profile:jira-service-management',
+ 'write:organization.profile:jira-service-management',
+ 'read:queue:jira-service-management',
+ 'read:request.sla:jira-service-management',
+ 'read:request.status:jira-service-management',
+ 'write:request.status:jira-service-management',
+ 'read:request.participant:jira-service-management',
+ 'write:request.participant:jira-service-management',
+ 'read:request.approval:jira-service-management',
+ 'write:request.approval:jira-service-management',
],
},
},
diff --git a/apps/sim/lib/tokenization/estimators.ts b/apps/sim/lib/tokenization/estimators.ts
index 57a084f61b..c0d96fb0f1 100644
--- a/apps/sim/lib/tokenization/estimators.ts
+++ b/apps/sim/lib/tokenization/estimators.ts
@@ -57,6 +57,40 @@ export function getAccurateTokenCount(text: string, modelName = 'text-embedding-
}
}
+/**
+ * Get individual tokens as strings for visualization
+ * Returns an array of token strings that can be displayed with colors
+ */
+export function getTokenStrings(text: string, modelName = 'text-embedding-3-small'): string[] {
+ if (!text || text.length === 0) {
+ return []
+ }
+
+ try {
+ const encoding = getEncoding(modelName)
+ const tokenIds = encoding.encode(text)
+
+ const textChars = [...text]
+ const result: string[] = []
+ let prevCharCount = 0
+
+ for (let i = 0; i < tokenIds.length; i++) {
+ const decoded = encoding.decode(tokenIds.slice(0, i + 1))
+ const currentCharCount = [...decoded].length
+ const tokenCharCount = currentCharCount - prevCharCount
+
+ const tokenStr = textChars.slice(prevCharCount, prevCharCount + tokenCharCount).join('')
+ result.push(tokenStr)
+ prevCharCount = currentCharCount
+ }
+
+ return result
+ } catch (error) {
+ logger.error('Error getting token strings:', error)
+ return text.split(/(\s+)/).filter((s) => s.length > 0)
+ }
+}
+
/**
* Truncate text to a maximum token count
* Useful for handling texts that exceed model limits
diff --git a/apps/sim/lib/workflows/triggers/trigger-utils.ts b/apps/sim/lib/workflows/triggers/trigger-utils.ts
index a4da45cd2b..4601f9f32c 100644
--- a/apps/sim/lib/workflows/triggers/trigger-utils.ts
+++ b/apps/sim/lib/workflows/triggers/trigger-utils.ts
@@ -10,6 +10,43 @@ import { getTrigger } from '@/triggers'
const logger = createLogger('TriggerUtils')
+/**
+ * Valid start block types that can trigger a workflow
+ */
+export const VALID_START_BLOCK_TYPES = [
+ 'starter',
+ 'start',
+ 'start_trigger',
+ 'api',
+ 'api_trigger',
+ 'input_trigger',
+] as const
+
+export type ValidStartBlockType = (typeof VALID_START_BLOCK_TYPES)[number]
+
+/**
+ * Check if a block type is a valid start block type
+ */
+export function isValidStartBlockType(blockType: string): blockType is ValidStartBlockType {
+ return VALID_START_BLOCK_TYPES.includes(blockType as ValidStartBlockType)
+}
+
+/**
+ * Check if a workflow state has a valid start block
+ */
+export function hasValidStartBlockInState(state: any): boolean {
+ if (!state?.blocks) {
+ return false
+ }
+
+ const startBlock = Object.values(state.blocks).find((block: any) => {
+ const blockType = block?.type
+ return isValidStartBlockType(blockType)
+ })
+
+ return !!startBlock
+}
+
/**
* Generates mock data based on the output type definition
*/
diff --git a/apps/sim/lib/workflows/types.ts b/apps/sim/lib/workflows/types.ts
index f288e2d4a9..6eb4b547ee 100644
--- a/apps/sim/lib/workflows/types.ts
+++ b/apps/sim/lib/workflows/types.ts
@@ -1,6 +1,7 @@
export interface InputFormatField {
name?: string
type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' | string
+ description?: string
value?: unknown
}
diff --git a/apps/sim/package.json b/apps/sim/package.json
index 27c580e418..93dcd5d52b 100644
--- a/apps/sim/package.json
+++ b/apps/sim/package.json
@@ -97,7 +97,6 @@
"ffmpeg-static": "5.3.0",
"fluent-ffmpeg": "2.1.3",
"framer-motion": "^12.5.0",
- "fuse.js": "7.1.0",
"google-auth-library": "10.5.0",
"gray-matter": "^4.0.3",
"groq-sdk": "^0.15.0",
diff --git a/apps/sim/stores/constants.ts b/apps/sim/stores/constants.ts
index 917ff0d11f..62e51f9d2c 100644
--- a/apps/sim/stores/constants.ts
+++ b/apps/sim/stores/constants.ts
@@ -59,3 +59,58 @@ export const COPILOT_TOOL_ERROR_NAMES: Record = {
} as const
export type CopilotToolId = keyof typeof COPILOT_TOOL_DISPLAY_NAMES
+
+/**
+ * Layout dimension constants.
+ *
+ * These values must stay in sync with:
+ * - `globals.css` (CSS variable defaults)
+ * - `layout.tsx` (blocking script validations)
+ *
+ * @see globals.css for CSS variable definitions
+ * @see layout.tsx for pre-hydration script that reads localStorage
+ */
+
+/** Sidebar width constraints */
+export const SIDEBAR_WIDTH = {
+ DEFAULT: 232,
+ MIN: 232,
+ /** Maximum is 30% of viewport, enforced dynamically */
+ MAX_PERCENTAGE: 0.3,
+} as const
+
+/** Right panel width constraints */
+export const PANEL_WIDTH = {
+ DEFAULT: 290,
+ MIN: 290,
+ /** Maximum is 40% of viewport, enforced dynamically */
+ MAX_PERCENTAGE: 0.4,
+} as const
+
+/** Terminal height constraints */
+export const TERMINAL_HEIGHT = {
+ DEFAULT: 155,
+ MIN: 30,
+ /** Maximum is 70% of viewport, enforced dynamically */
+ MAX_PERCENTAGE: 0.7,
+} as const
+
+/** Toolbar triggers section height constraints */
+export const TOOLBAR_TRIGGERS_HEIGHT = {
+ DEFAULT: 300,
+ MIN: 30,
+ MAX: 800,
+} as const
+
+/** Editor connections section height constraints */
+export const EDITOR_CONNECTIONS_HEIGHT = {
+ DEFAULT: 172,
+ MIN: 30,
+ MAX: 300,
+} as const
+
+/** Output panel (terminal execution results) width constraints */
+export const OUTPUT_PANEL_WIDTH = {
+ DEFAULT: 440,
+ MIN: 440,
+} as const
diff --git a/apps/sim/stores/knowledge/store.ts b/apps/sim/stores/knowledge/store.ts
deleted file mode 100644
index 40c3eef172..0000000000
--- a/apps/sim/stores/knowledge/store.ts
+++ /dev/null
@@ -1,923 +0,0 @@
-import { createLogger } from '@sim/logger'
-import { create } from 'zustand'
-
-const logger = createLogger('KnowledgeStore')
-
-/**
- * Configuration for document chunking in knowledge bases
- *
- * Units:
- * - maxSize: Maximum chunk size in TOKENS (1 token ≈ 4 characters)
- * - minSize: Minimum chunk size in CHARACTERS (floor to avoid tiny fragments)
- * - overlap: Overlap between chunks in TOKENS (1 token ≈ 4 characters)
- */
-export interface ChunkingConfig {
- /** Maximum chunk size in tokens (default: 1024, range: 100-4000) */
- maxSize: number
- /** Minimum chunk size in characters (default: 100, range: 1-2000) */
- minSize: number
- /** Overlap between chunks in tokens (default: 200, range: 0-500) */
- overlap: number
- chunkSize?: number // Legacy support
- minCharactersPerChunk?: number // Legacy support
- recipe?: string
- lang?: string
- strategy?: 'recursive' | 'semantic' | 'sentence' | 'paragraph'
- [key: string]: unknown
-}
-
-export interface KnowledgeBaseData {
- id: string
- name: string
- description?: string
- tokenCount: number
- embeddingModel: string
- embeddingDimension: number
- chunkingConfig: ChunkingConfig
- createdAt: string
- updatedAt: string
- workspaceId?: string
-}
-
-export interface DocumentData {
- id: string
- knowledgeBaseId: string
- filename: string
- fileUrl: string
- fileSize: number
- mimeType: string
- chunkCount: number
- tokenCount: number
- characterCount: number
- processingStatus: 'pending' | 'processing' | 'completed' | 'failed'
- processingStartedAt?: string | null
- processingCompletedAt?: string | null
- processingError?: string | null
- enabled: boolean
- uploadedAt: string
- // Text tags
- tag1?: string | null
- tag2?: string | null
- tag3?: string | null
- tag4?: string | null
- tag5?: string | null
- tag6?: string | null
- tag7?: string | null
- // Number tags (5 slots)
- number1?: number | null
- number2?: number | null
- number3?: number | null
- number4?: number | null
- number5?: number | null
- // Date tags (2 slots)
- date1?: string | null
- date2?: string | null
- // Boolean tags (3 slots)
- boolean1?: boolean | null
- boolean2?: boolean | null
- boolean3?: boolean | null
-}
-
-export interface ChunkData {
- id: string
- chunkIndex: number
- content: string
- contentLength: number
- tokenCount: number
- enabled: boolean
- startOffset: number
- endOffset: number
- // Text tags
- tag1?: string | null
- tag2?: string | null
- tag3?: string | null
- tag4?: string | null
- tag5?: string | null
- tag6?: string | null
- tag7?: string | null
- // Number tags (5 slots)
- number1?: number | null
- number2?: number | null
- number3?: number | null
- number4?: number | null
- number5?: number | null
- // Date tags (2 slots)
- date1?: string | null
- date2?: string | null
- // Boolean tags (3 slots)
- boolean1?: boolean | null
- boolean2?: boolean | null
- boolean3?: boolean | null
- createdAt: string
- updatedAt: string
-}
-
-export interface ChunksPagination {
- total: number
- limit: number
- offset: number
- hasMore: boolean
-}
-
-export interface ChunksCache {
- chunks: ChunkData[]
- pagination: ChunksPagination
- searchQuery?: string
- lastFetchTime: number
-}
-
-export interface DocumentsPagination {
- total: number
- limit: number
- offset: number
- hasMore: boolean
-}
-
-export interface DocumentsCache {
- documents: DocumentData[]
- pagination: DocumentsPagination
- searchQuery?: string
- sortBy?: string
- sortOrder?: string
- lastFetchTime: number
-}
-
-interface KnowledgeStore {
- // State
- knowledgeBases: Record
- documents: Record // knowledgeBaseId -> documents cache
- chunks: Record // documentId -> chunks cache
- knowledgeBasesList: KnowledgeBaseData[]
-
- // Loading states
- loadingKnowledgeBases: Set
- loadingDocuments: Set
- loadingChunks: Set
- loadingKnowledgeBasesList: boolean
- knowledgeBasesListLoaded: boolean
-
- // Actions
- getKnowledgeBase: (id: string) => Promise
- getDocuments: (
- knowledgeBaseId: string,
- options?: {
- search?: string
- limit?: number
- offset?: number
- sortBy?: string
- sortOrder?: string
- }
- ) => Promise
- getChunks: (
- knowledgeBaseId: string,
- documentId: string,
- options?: { search?: string; limit?: number; offset?: number }
- ) => Promise
- getKnowledgeBasesList: (workspaceId?: string) => Promise
- refreshDocuments: (
- knowledgeBaseId: string,
- options?: {
- search?: string
- limit?: number
- offset?: number
- sortBy?: string
- sortOrder?: string
- }
- ) => Promise
- refreshChunks: (
- knowledgeBaseId: string,
- documentId: string,
- options?: { search?: string; limit?: number; offset?: number }
- ) => Promise
- updateDocument: (
- knowledgeBaseId: string,
- documentId: string,
- updates: Partial
- ) => void
- updateChunk: (documentId: string, chunkId: string, updates: Partial) => void
- addPendingDocuments: (knowledgeBaseId: string, documents: DocumentData[]) => void
- addKnowledgeBase: (knowledgeBase: KnowledgeBaseData) => void
- updateKnowledgeBase: (id: string, updates: Partial) => void
- removeKnowledgeBase: (id: string) => void
- removeDocument: (knowledgeBaseId: string, documentId: string) => void
- clearDocuments: (knowledgeBaseId: string) => void
- clearChunks: (documentId: string) => void
- clearKnowledgeBasesList: () => void
-
- // Getters
- getCachedKnowledgeBase: (id: string) => KnowledgeBaseData | null
- getCachedDocuments: (knowledgeBaseId: string) => DocumentsCache | null
- getCachedChunks: (documentId: string, options?: { search?: string }) => ChunksCache | null
-
- // Loading state getters
- isKnowledgeBaseLoading: (id: string) => boolean
- isDocumentsLoading: (knowledgeBaseId: string) => boolean
- isChunksLoading: (documentId: string) => boolean
-}
-
-export const useKnowledgeStore = create((set, get) => ({
- knowledgeBases: {},
- documents: {},
- chunks: {},
- knowledgeBasesList: [],
- loadingKnowledgeBases: new Set(),
- loadingDocuments: new Set(),
- loadingChunks: new Set(),
- loadingKnowledgeBasesList: false,
- knowledgeBasesListLoaded: false,
-
- getCachedKnowledgeBase: (id: string) => {
- return get().knowledgeBases[id] || null
- },
-
- getCachedDocuments: (knowledgeBaseId: string) => {
- return get().documents[knowledgeBaseId] || null
- },
-
- getCachedChunks: (documentId: string, options?: { search?: string }) => {
- return get().chunks[documentId] || null
- },
-
- isKnowledgeBaseLoading: (id: string) => {
- return get().loadingKnowledgeBases.has(id)
- },
-
- isDocumentsLoading: (knowledgeBaseId: string) => {
- return get().loadingDocuments.has(knowledgeBaseId)
- },
-
- isChunksLoading: (documentId: string) => {
- return get().loadingChunks.has(documentId)
- },
-
- getKnowledgeBase: async (id: string) => {
- const state = get()
-
- // Return cached data if it exists
- const cached = state.knowledgeBases[id]
- if (cached) {
- return cached
- }
-
- // Return cached data if already loading to prevent duplicate requests
- if (state.loadingKnowledgeBases.has(id)) {
- return null
- }
-
- try {
- set((state) => ({
- loadingKnowledgeBases: new Set([...state.loadingKnowledgeBases, id]),
- }))
-
- const response = await fetch(`/api/knowledge/${id}`)
-
- if (!response.ok) {
- throw new Error(`Failed to fetch knowledge base: ${response.statusText}`)
- }
-
- const result = await response.json()
-
- if (!result.success) {
- throw new Error(result.error || 'Failed to fetch knowledge base')
- }
-
- const knowledgeBase = result.data
-
- set((state) => ({
- knowledgeBases: {
- ...state.knowledgeBases,
- [id]: knowledgeBase,
- },
- loadingKnowledgeBases: new Set(
- [...state.loadingKnowledgeBases].filter((loadingId) => loadingId !== id)
- ),
- }))
-
- logger.info(`Knowledge base loaded: ${id}`)
- return knowledgeBase
- } catch (error) {
- logger.error(`Error fetching knowledge base ${id}:`, error)
-
- set((state) => ({
- loadingKnowledgeBases: new Set(
- [...state.loadingKnowledgeBases].filter((loadingId) => loadingId !== id)
- ),
- }))
-
- throw error
- }
- },
-
- getDocuments: async (
- knowledgeBaseId: string,
- options?: {
- search?: string
- limit?: number
- offset?: number
- sortBy?: string
- sortOrder?: string
- }
- ) => {
- const state = get()
-
- // Check if we have cached data that matches the exact request parameters
- const cached = state.documents[knowledgeBaseId]
- const requestLimit = options?.limit || 50
- const requestOffset = options?.offset || 0
- const requestSearch = options?.search
- const requestSortBy = options?.sortBy
- const requestSortOrder = options?.sortOrder
-
- if (
- cached &&
- cached.searchQuery === requestSearch &&
- cached.pagination.limit === requestLimit &&
- cached.pagination.offset === requestOffset &&
- cached.sortBy === requestSortBy &&
- cached.sortOrder === requestSortOrder
- ) {
- return cached.documents
- }
-
- // Return empty array if already loading to prevent duplicate requests
- if (state.loadingDocuments.has(knowledgeBaseId)) {
- return cached?.documents || []
- }
-
- try {
- set((state) => ({
- loadingDocuments: new Set([...state.loadingDocuments, knowledgeBaseId]),
- }))
-
- // Build query parameters using the same defaults as caching
- const params = new URLSearchParams()
- if (requestSearch) params.set('search', requestSearch)
- if (requestSortBy) params.set('sortBy', requestSortBy)
- if (requestSortOrder) params.set('sortOrder', requestSortOrder)
- params.set('limit', requestLimit.toString())
- params.set('offset', requestOffset.toString())
-
- const url = `/api/knowledge/${knowledgeBaseId}/documents${params.toString() ? `?${params.toString()}` : ''}`
- const response = await fetch(url)
-
- if (!response.ok) {
- throw new Error(`Failed to fetch documents: ${response.statusText}`)
- }
-
- const result = await response.json()
-
- if (!result.success) {
- throw new Error(result.error || 'Failed to fetch documents')
- }
-
- const documents = result.data.documents || result.data // Handle both paginated and non-paginated responses
- const pagination = result.data.pagination || {
- total: documents.length,
- limit: requestLimit,
- offset: requestOffset,
- hasMore: false,
- }
-
- const documentsCache: DocumentsCache = {
- documents,
- pagination,
- searchQuery: requestSearch,
- sortBy: requestSortBy,
- sortOrder: requestSortOrder,
- lastFetchTime: Date.now(),
- }
-
- set((state) => ({
- documents: {
- ...state.documents,
- [knowledgeBaseId]: documentsCache,
- },
- loadingDocuments: new Set(
- [...state.loadingDocuments].filter((loadingId) => loadingId !== knowledgeBaseId)
- ),
- }))
-
- logger.info(`Documents loaded for knowledge base: ${knowledgeBaseId}`)
- return documents
- } catch (error) {
- logger.error(`Error fetching documents for knowledge base ${knowledgeBaseId}:`, error)
-
- set((state) => ({
- loadingDocuments: new Set(
- [...state.loadingDocuments].filter((loadingId) => loadingId !== knowledgeBaseId)
- ),
- }))
-
- throw error
- }
- },
-
- getChunks: async (
- knowledgeBaseId: string,
- documentId: string,
- options?: { search?: string; limit?: number; offset?: number }
- ) => {
- const state = get()
-
- // Return cached chunks if they exist and match the exact search criteria AND offset
- const cached = state.chunks[documentId]
- if (
- cached &&
- cached.searchQuery === options?.search &&
- cached.pagination.offset === (options?.offset || 0) &&
- cached.pagination.limit === (options?.limit || 50)
- ) {
- return cached.chunks
- }
-
- // Return empty array if already loading to prevent duplicate requests
- if (state.loadingChunks.has(documentId)) {
- return cached?.chunks || []
- }
-
- try {
- set((state) => ({
- loadingChunks: new Set([...state.loadingChunks, documentId]),
- }))
-
- // Build query parameters
- const params = new URLSearchParams()
- if (options?.search) params.set('search', options.search)
- if (options?.limit) params.set('limit', options.limit.toString())
- if (options?.offset) params.set('offset', options.offset.toString())
-
- const response = await fetch(
- `/api/knowledge/${knowledgeBaseId}/documents/${documentId}/chunks?${params.toString()}`
- )
-
- if (!response.ok) {
- throw new Error(`Failed to fetch chunks: ${response.statusText}`)
- }
-
- const result = await response.json()
-
- if (!result.success) {
- throw new Error(result.error || 'Failed to fetch chunks')
- }
-
- const chunks = result.data
- const pagination = result.pagination
-
- set((state) => ({
- chunks: {
- ...state.chunks,
- [documentId]: {
- chunks, // Always replace chunks for traditional pagination
- pagination: {
- total: pagination?.total || chunks.length,
- limit: pagination?.limit || options?.limit || 50,
- offset: pagination?.offset || options?.offset || 0,
- hasMore: pagination?.hasMore || false,
- },
- searchQuery: options?.search,
- lastFetchTime: Date.now(),
- },
- },
- loadingChunks: new Set(
- [...state.loadingChunks].filter((loadingId) => loadingId !== documentId)
- ),
- }))
-
- logger.info(`Chunks loaded for document: ${documentId}`)
- return chunks
- } catch (error) {
- logger.error(`Error fetching chunks for document ${documentId}:`, error)
-
- set((state) => ({
- loadingChunks: new Set(
- [...state.loadingChunks].filter((loadingId) => loadingId !== documentId)
- ),
- }))
-
- throw error
- }
- },
-
- getKnowledgeBasesList: async (workspaceId?: string) => {
- const state = get()
-
- // Return cached list if we have already loaded it before (prevents infinite loops when empty)
- if (state.knowledgeBasesListLoaded) {
- return state.knowledgeBasesList
- }
-
- // Return cached data if already loading
- if (state.loadingKnowledgeBasesList) {
- return state.knowledgeBasesList
- }
-
- // Create an AbortController for request cancellation
- const abortController = new AbortController()
- const timeoutId = setTimeout(() => {
- abortController.abort()
- }, 10000) // 10 second timeout
-
- try {
- set({ loadingKnowledgeBasesList: true })
-
- const url = workspaceId ? `/api/knowledge?workspaceId=${workspaceId}` : '/api/knowledge'
- const response = await fetch(url, {
- signal: abortController.signal,
- headers: {
- 'Content-Type': 'application/json',
- },
- })
-
- // Clear the timeout since request completed
- clearTimeout(timeoutId)
-
- if (!response.ok) {
- throw new Error(
- `Failed to fetch knowledge bases: ${response.status} ${response.statusText}`
- )
- }
-
- const result = await response.json()
-
- if (!result.success) {
- throw new Error(result.error || 'Failed to fetch knowledge bases')
- }
-
- const knowledgeBasesList = result.data || []
-
- set({
- knowledgeBasesList,
- loadingKnowledgeBasesList: false,
- knowledgeBasesListLoaded: true, // Mark as loaded regardless of result to prevent infinite loops
- })
-
- logger.info(`Knowledge bases list loaded: ${knowledgeBasesList.length} items`)
- return knowledgeBasesList
- } catch (error) {
- // Clear the timeout in case of error
- clearTimeout(timeoutId)
-
- logger.error('Error fetching knowledge bases list:', error)
-
- // Always set loading to false, even on error
- set({
- loadingKnowledgeBasesList: false,
- knowledgeBasesListLoaded: true, // Mark as loaded even on error to prevent infinite retries
- })
-
- // Don't throw on AbortError (timeout or cancellation)
- if (error instanceof Error && error.name === 'AbortError') {
- logger.warn('Knowledge bases list request was aborted (timeout or cancellation)')
- return state.knowledgeBasesList // Return whatever we have cached
- }
-
- throw error
- }
- },
-
- refreshDocuments: async (
- knowledgeBaseId: string,
- options?: {
- search?: string
- limit?: number
- offset?: number
- sortBy?: string
- sortOrder?: string
- }
- ) => {
- const state = get()
-
- // Return empty array if already loading to prevent duplicate requests
- if (state.loadingDocuments.has(knowledgeBaseId)) {
- return state.documents[knowledgeBaseId]?.documents || []
- }
-
- try {
- set((state) => ({
- loadingDocuments: new Set([...state.loadingDocuments, knowledgeBaseId]),
- }))
-
- // Build query parameters using consistent defaults
- const requestLimit = options?.limit || 50
- const requestOffset = options?.offset || 0
- const requestSearch = options?.search
- const requestSortBy = options?.sortBy
- const requestSortOrder = options?.sortOrder
-
- const params = new URLSearchParams()
- if (requestSearch) params.set('search', requestSearch)
- if (requestSortBy) params.set('sortBy', requestSortBy)
- if (requestSortOrder) params.set('sortOrder', requestSortOrder)
- params.set('limit', requestLimit.toString())
- params.set('offset', requestOffset.toString())
-
- const url = `/api/knowledge/${knowledgeBaseId}/documents${params.toString() ? `?${params.toString()}` : ''}`
- const response = await fetch(url)
-
- if (!response.ok) {
- throw new Error(`Failed to fetch documents: ${response.statusText}`)
- }
-
- const result = await response.json()
-
- if (!result.success) {
- throw new Error(result.error || 'Failed to fetch documents')
- }
-
- const documents = result.data.documents || result.data
- const pagination = result.data.pagination || {
- total: documents.length,
- limit: requestLimit,
- offset: requestOffset,
- hasMore: false,
- }
-
- const documentsCache: DocumentsCache = {
- documents,
- pagination,
- searchQuery: requestSearch,
- sortBy: requestSortBy,
- sortOrder: requestSortOrder,
- lastFetchTime: Date.now(),
- }
-
- set((state) => ({
- documents: {
- ...state.documents,
- [knowledgeBaseId]: documentsCache,
- },
- loadingDocuments: new Set(
- [...state.loadingDocuments].filter((loadingId) => loadingId !== knowledgeBaseId)
- ),
- }))
-
- logger.info(`Documents refreshed for knowledge base: ${knowledgeBaseId}`)
- return documents
- } catch (error) {
- logger.error(`Error refreshing documents for knowledge base ${knowledgeBaseId}:`, error)
-
- set((state) => ({
- loadingDocuments: new Set(
- [...state.loadingDocuments].filter((loadingId) => loadingId !== knowledgeBaseId)
- ),
- }))
-
- throw error
- }
- },
-
- refreshChunks: async (
- knowledgeBaseId: string,
- documentId: string,
- options?: { search?: string; limit?: number; offset?: number }
- ) => {
- const state = get()
-
- // Return cached chunks if already loading to prevent duplicate requests
- if (state.loadingChunks.has(documentId)) {
- return state.chunks[documentId]?.chunks || []
- }
-
- try {
- set((state) => ({
- loadingChunks: new Set([...state.loadingChunks, documentId]),
- }))
-
- // Build query parameters - for refresh, always start from offset 0
- const params = new URLSearchParams()
- if (options?.search) params.set('search', options.search)
- if (options?.limit) params.set('limit', options.limit.toString())
- params.set('offset', '0') // Always start fresh on refresh
-
- const response = await fetch(
- `/api/knowledge/${knowledgeBaseId}/documents/${documentId}/chunks?${params.toString()}`
- )
-
- if (!response.ok) {
- throw new Error(`Failed to fetch chunks: ${response.statusText}`)
- }
-
- const result = await response.json()
-
- if (!result.success) {
- throw new Error(result.error || 'Failed to fetch chunks')
- }
-
- const chunks = result.data
- const pagination = result.pagination
-
- set((state) => ({
- chunks: {
- ...state.chunks,
- [documentId]: {
- chunks, // Replace all chunks with fresh data
- pagination: {
- total: pagination?.total || chunks.length,
- limit: pagination?.limit || options?.limit || 50,
- offset: 0, // Reset to start
- hasMore: pagination?.hasMore || false,
- },
- searchQuery: options?.search,
- lastFetchTime: Date.now(),
- },
- },
- loadingChunks: new Set(
- [...state.loadingChunks].filter((loadingId) => loadingId !== documentId)
- ),
- }))
-
- logger.info(`Chunks refreshed for document: ${documentId}`)
- return chunks
- } catch (error) {
- logger.error(`Error refreshing chunks for document ${documentId}:`, error)
-
- set((state) => ({
- loadingChunks: new Set(
- [...state.loadingChunks].filter((loadingId) => loadingId !== documentId)
- ),
- }))
-
- throw error
- }
- },
-
- updateDocument: (knowledgeBaseId: string, documentId: string, updates: Partial) => {
- set((state) => {
- const documentsCache = state.documents[knowledgeBaseId]
- if (!documentsCache) return state
-
- const updatedDocuments = documentsCache.documents.map((doc) =>
- doc.id === documentId ? { ...doc, ...updates } : doc
- )
-
- return {
- documents: {
- ...state.documents,
- [knowledgeBaseId]: {
- ...documentsCache,
- documents: updatedDocuments,
- },
- },
- }
- })
- },
-
- updateChunk: (documentId: string, chunkId: string, updates: Partial) => {
- set((state) => {
- const cachedChunks = state.chunks[documentId]
- if (!cachedChunks || !cachedChunks.chunks) return state
-
- const updatedChunks = cachedChunks.chunks.map((chunk) =>
- chunk.id === chunkId ? { ...chunk, ...updates } : chunk
- )
-
- return {
- chunks: {
- ...state.chunks,
- [documentId]: {
- ...cachedChunks,
- chunks: updatedChunks,
- },
- },
- }
- })
- },
-
- addPendingDocuments: (knowledgeBaseId: string, newDocuments: DocumentData[]) => {
- set((state) => {
- const existingDocumentsCache = state.documents[knowledgeBaseId]
- const existingDocuments = existingDocumentsCache?.documents || []
-
- const existingIds = new Set(existingDocuments.map((doc) => doc.id))
- const uniqueNewDocuments = newDocuments.filter((doc) => !existingIds.has(doc.id))
-
- if (uniqueNewDocuments.length === 0) {
- logger.warn(`No new documents to add - all ${newDocuments.length} documents already exist`)
- return state
- }
-
- const updatedDocuments = [...existingDocuments, ...uniqueNewDocuments]
-
- const documentsCache: DocumentsCache = {
- documents: updatedDocuments,
- pagination: {
- ...(existingDocumentsCache?.pagination || {
- limit: 50,
- offset: 0,
- hasMore: false,
- }),
- total: updatedDocuments.length,
- },
- searchQuery: existingDocumentsCache?.searchQuery,
- lastFetchTime: Date.now(),
- }
-
- return {
- documents: {
- ...state.documents,
- [knowledgeBaseId]: documentsCache,
- },
- }
- })
- logger.info(
- `Added ${newDocuments.filter((doc) => !get().documents[knowledgeBaseId]?.documents?.some((existing) => existing.id === doc.id)).length} pending documents for knowledge base: ${knowledgeBaseId}`
- )
- },
-
- addKnowledgeBase: (knowledgeBase: KnowledgeBaseData) => {
- set((state) => ({
- knowledgeBases: {
- ...state.knowledgeBases,
- [knowledgeBase.id]: knowledgeBase,
- },
- knowledgeBasesList: [knowledgeBase, ...state.knowledgeBasesList],
- }))
- logger.info(`Knowledge base added: ${knowledgeBase.id}`)
- },
-
- updateKnowledgeBase: (id: string, updates: Partial) => {
- set((state) => {
- const existingKb = state.knowledgeBases[id]
- if (!existingKb) return state
-
- const updatedKb = { ...existingKb, ...updates }
-
- return {
- knowledgeBases: {
- ...state.knowledgeBases,
- [id]: updatedKb,
- },
- knowledgeBasesList: state.knowledgeBasesList.map((kb) => (kb.id === id ? updatedKb : kb)),
- }
- })
- logger.info(`Knowledge base updated: ${id}`)
- },
-
- removeKnowledgeBase: (id: string) => {
- set((state) => {
- const newKnowledgeBases = { ...state.knowledgeBases }
- delete newKnowledgeBases[id]
-
- const newDocuments = { ...state.documents }
- delete newDocuments[id]
-
- return {
- knowledgeBases: newKnowledgeBases,
- documents: newDocuments,
- knowledgeBasesList: state.knowledgeBasesList.filter((kb) => kb.id !== id),
- }
- })
- logger.info(`Knowledge base removed: ${id}`)
- },
-
- removeDocument: (knowledgeBaseId: string, documentId: string) => {
- set((state) => {
- const documentsCache = state.documents[knowledgeBaseId]
- if (!documentsCache) return state
-
- const updatedDocuments = documentsCache.documents.filter((doc) => doc.id !== documentId)
-
- // Also clear chunks for the removed document
- const newChunks = { ...state.chunks }
- delete newChunks[documentId]
-
- return {
- documents: {
- ...state.documents,
- [knowledgeBaseId]: {
- ...documentsCache,
- documents: updatedDocuments,
- },
- },
- chunks: newChunks,
- }
- })
- logger.info(`Document removed from knowledge base: ${documentId}`)
- },
-
- clearDocuments: (knowledgeBaseId: string) => {
- set((state) => {
- const newDocuments = { ...state.documents }
- delete newDocuments[knowledgeBaseId]
- return { documents: newDocuments }
- })
- logger.info(`Documents cleared for knowledge base: ${knowledgeBaseId}`)
- },
-
- clearChunks: (documentId: string) => {
- set((state) => {
- const newChunks = { ...state.chunks }
- delete newChunks[documentId]
- return { chunks: newChunks }
- })
- logger.info(`Chunks cleared for document: ${documentId}`)
- },
-
- clearKnowledgeBasesList: () => {
- set({
- knowledgeBasesList: [],
- knowledgeBasesListLoaded: false, // Reset loaded state to allow reloading
- })
- logger.info('Knowledge bases list cleared')
- },
-}))
diff --git a/apps/sim/stores/logs/filters/store.ts b/apps/sim/stores/logs/filters/store.ts
index 6afadfc79a..0b9953d705 100644
--- a/apps/sim/stores/logs/filters/store.ts
+++ b/apps/sim/stores/logs/filters/store.ts
@@ -37,6 +37,8 @@ const parseTimeRangeFromURL = (value: string | null): TimeRange => {
return 'Past 14 days'
case 'past-30-days':
return 'Past 30 days'
+ case 'custom':
+ return 'Custom range'
default:
return DEFAULT_TIME_RANGE
}
@@ -57,7 +59,9 @@ const parseTriggerArrayFromURL = (value: string | null): TriggerType[] => {
if (!value) return []
return value
.split(',')
- .filter((t): t is TriggerType => ['chat', 'api', 'webhook', 'manual', 'schedule'].includes(t))
+ .filter((t): t is TriggerType =>
+ ['chat', 'api', 'webhook', 'manual', 'schedule', 'mcp'].includes(t)
+ )
}
const parseStringArrayFromURL = (value: string | null): string[] => {
@@ -85,6 +89,8 @@ const timeRangeToURL = (timeRange: TimeRange): string => {
return 'past-14-days'
case 'Past 30 days':
return 'past-30-days'
+ case 'Custom range':
+ return 'custom'
default:
return 'all-time'
}
@@ -94,6 +100,8 @@ export const useFilterStore = create((set, get) => ({
workspaceId: '',
viewMode: 'logs',
timeRange: DEFAULT_TIME_RANGE,
+ startDate: undefined,
+ endDate: undefined,
level: 'all',
workflowIds: [],
folderIds: [],
@@ -112,6 +120,28 @@ export const useFilterStore = create((set, get) => ({
}
},
+ setDateRange: (start, end) => {
+ set({
+ timeRange: 'Custom range',
+ startDate: start,
+ endDate: end,
+ })
+ if (!get().isInitializing) {
+ get().syncWithURL()
+ }
+ },
+
+ clearDateRange: () => {
+ set({
+ timeRange: DEFAULT_TIME_RANGE,
+ startDate: undefined,
+ endDate: undefined,
+ })
+ if (!get().isInitializing) {
+ get().syncWithURL()
+ }
+ },
+
setLevel: (level) => {
set({ level })
if (!get().isInitializing) {
@@ -205,9 +235,13 @@ export const useFilterStore = create((set, get) => ({
const folderIds = parseStringArrayFromURL(params.get('folderIds'))
const triggers = parseTriggerArrayFromURL(params.get('triggers'))
const searchQuery = params.get('search') || ''
+ const startDate = params.get('startDate') || undefined
+ const endDate = params.get('endDate') || undefined
set({
timeRange,
+ startDate,
+ endDate,
level,
workflowIds,
folderIds,
@@ -218,13 +252,23 @@ export const useFilterStore = create((set, get) => ({
},
syncWithURL: () => {
- const { timeRange, level, workflowIds, folderIds, triggers, searchQuery } = get()
+ const { timeRange, startDate, endDate, level, workflowIds, folderIds, triggers, searchQuery } =
+ get()
const params = new URLSearchParams()
if (timeRange !== DEFAULT_TIME_RANGE) {
params.set('timeRange', timeRangeToURL(timeRange))
}
+ if (timeRange === 'Custom range') {
+ if (startDate) {
+ params.set('startDate', startDate)
+ }
+ if (endDate) {
+ params.set('endDate', endDate)
+ }
+ }
+
if (level !== 'all') {
params.set('level', level)
}
diff --git a/apps/sim/stores/logs/filters/types.ts b/apps/sim/stores/logs/filters/types.ts
index 0df25ccec3..41af15bb4f 100644
--- a/apps/sim/stores/logs/filters/types.ts
+++ b/apps/sim/stores/logs/filters/types.ts
@@ -170,15 +170,26 @@ export type TimeRange =
| 'Past 14 days'
| 'Past 30 days'
| 'All time'
+ | 'Custom range'
export type LogLevel = 'error' | 'info' | 'running' | 'pending' | 'all' | (string & {})
-export type TriggerType = 'chat' | 'api' | 'webhook' | 'manual' | 'schedule' | 'all' | string
+export type TriggerType =
+ | 'chat'
+ | 'api'
+ | 'webhook'
+ | 'manual'
+ | 'schedule'
+ | 'mcp'
+ | 'all'
+ | (string & {})
/** Filter state for logs and dashboard views */
export interface FilterState {
workspaceId: string
viewMode: 'logs' | 'dashboard'
timeRange: TimeRange
+ startDate?: string
+ endDate?: string
level: LogLevel
workflowIds: string[]
folderIds: string[]
@@ -189,6 +200,8 @@ export interface FilterState {
setWorkspaceId: (workspaceId: string) => void
setViewMode: (viewMode: 'logs' | 'dashboard') => void
setTimeRange: (timeRange: TimeRange) => void
+ setDateRange: (startDate: string | undefined, endDate: string | undefined) => void
+ clearDateRange: () => void
setLevel: (level: LogLevel) => void
setWorkflowIds: (workflowIds: string[]) => void
toggleWorkflowId: (workflowId: string) => void
diff --git a/apps/sim/stores/panel/editor/store.ts b/apps/sim/stores/panel/editor/store.ts
index d03e91f5de..0e3ff198a6 100644
--- a/apps/sim/stores/panel/editor/store.ts
+++ b/apps/sim/stores/panel/editor/store.ts
@@ -2,15 +2,9 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
+import { EDITOR_CONNECTIONS_HEIGHT } from '@/stores/constants'
import { usePanelStore } from '../store'
-/**
- * Connections height constraints
- */
-const DEFAULT_CONNECTIONS_HEIGHT = 172
-const MIN_CONNECTIONS_HEIGHT = 30
-const MAX_CONNECTIONS_HEIGHT = 300
-
/**
* State for the Editor panel.
* Tracks the currently selected block to edit its subblocks/values and connections panel height.
@@ -38,7 +32,7 @@ export const usePanelEditorStore = create()(
persist(
(set, get) => ({
currentBlockId: null,
- connectionsHeight: DEFAULT_CONNECTIONS_HEIGHT,
+ connectionsHeight: EDITOR_CONNECTIONS_HEIGHT.DEFAULT,
setCurrentBlockId: (blockId) => {
set({ currentBlockId: blockId })
@@ -53,8 +47,8 @@ export const usePanelEditorStore = create()(
},
setConnectionsHeight: (height) => {
const clampedHeight = Math.max(
- MIN_CONNECTIONS_HEIGHT,
- Math.min(MAX_CONNECTIONS_HEIGHT, height)
+ EDITOR_CONNECTIONS_HEIGHT.MIN,
+ Math.min(EDITOR_CONNECTIONS_HEIGHT.MAX, height)
)
set({ connectionsHeight: clampedHeight })
// Update CSS variable for immediate visual feedback
@@ -68,7 +62,9 @@ export const usePanelEditorStore = create()(
toggleConnectionsCollapsed: () => {
const currentState = get()
const isAtMinHeight = currentState.connectionsHeight <= 35
- const newHeight = isAtMinHeight ? DEFAULT_CONNECTIONS_HEIGHT : MIN_CONNECTIONS_HEIGHT
+ const newHeight = isAtMinHeight
+ ? EDITOR_CONNECTIONS_HEIGHT.DEFAULT
+ : EDITOR_CONNECTIONS_HEIGHT.MIN
set({ connectionsHeight: newHeight })
@@ -88,7 +84,7 @@ export const usePanelEditorStore = create()(
if (state && typeof window !== 'undefined') {
document.documentElement.style.setProperty(
'--editor-connections-height',
- `${state.connectionsHeight || DEFAULT_CONNECTIONS_HEIGHT}px`
+ `${state.connectionsHeight || EDITOR_CONNECTIONS_HEIGHT.DEFAULT}px`
)
}
},
diff --git a/apps/sim/stores/panel/store.ts b/apps/sim/stores/panel/store.ts
index 8e3571f7e3..dfa2b8fcd9 100644
--- a/apps/sim/stores/panel/store.ts
+++ b/apps/sim/stores/panel/store.ts
@@ -1,13 +1,8 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
+import { PANEL_WIDTH } from '@/stores/constants'
import type { PanelState, PanelTab } from '@/stores/panel/types'
-/**
- * Panel width constraints
- * Note: Maximum width is enforced dynamically at 40% of viewport width in the resize hook
- */
-const MIN_PANEL_WIDTH = 260
-
/**
* Default panel tab
*/
@@ -16,10 +11,10 @@ const DEFAULT_TAB: PanelTab = 'copilot'
export const usePanelStore = create()(
persist(
(set) => ({
- panelWidth: MIN_PANEL_WIDTH,
+ panelWidth: PANEL_WIDTH.DEFAULT,
setPanelWidth: (width) => {
// Only enforce minimum - maximum is enforced dynamically by the resize hook
- const clampedWidth = Math.max(MIN_PANEL_WIDTH, width)
+ const clampedWidth = Math.max(PANEL_WIDTH.MIN, width)
set({ panelWidth: clampedWidth })
// Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') {
diff --git a/apps/sim/stores/panel/toolbar/store.ts b/apps/sim/stores/panel/toolbar/store.ts
index 4168c5c145..7b10d73b15 100644
--- a/apps/sim/stores/panel/toolbar/store.ts
+++ b/apps/sim/stores/panel/toolbar/store.ts
@@ -1,13 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
-
-/**
- * Toolbar triggers height constraints
- * Minimum is set low to allow collapsing to just the header height (~30-40px)
- */
-const DEFAULT_TOOLBAR_TRIGGERS_HEIGHT = 300
-const MIN_TOOLBAR_HEIGHT = 30
-const MAX_TOOLBAR_HEIGHT = 800
+import { TOOLBAR_TRIGGERS_HEIGHT } from '@/stores/constants'
/**
* Toolbar state interface
@@ -22,9 +15,12 @@ interface ToolbarState {
export const useToolbarStore = create()(
persist(
(set) => ({
- toolbarTriggersHeight: DEFAULT_TOOLBAR_TRIGGERS_HEIGHT,
+ toolbarTriggersHeight: TOOLBAR_TRIGGERS_HEIGHT.DEFAULT,
setToolbarTriggersHeight: (height) => {
- const clampedHeight = Math.max(MIN_TOOLBAR_HEIGHT, Math.min(MAX_TOOLBAR_HEIGHT, height))
+ const clampedHeight = Math.max(
+ TOOLBAR_TRIGGERS_HEIGHT.MIN,
+ Math.min(TOOLBAR_TRIGGERS_HEIGHT.MAX, height)
+ )
set({ toolbarTriggersHeight: clampedHeight })
// Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') {
@@ -44,7 +40,7 @@ export const useToolbarStore = create()(
if (state && typeof window !== 'undefined') {
document.documentElement.style.setProperty(
'--toolbar-triggers-height',
- `${state.toolbarTriggersHeight || DEFAULT_TOOLBAR_TRIGGERS_HEIGHT}px`
+ `${state.toolbarTriggersHeight || TOOLBAR_TRIGGERS_HEIGHT.DEFAULT}px`
)
}
},
diff --git a/apps/sim/stores/settings-modal/store.ts b/apps/sim/stores/settings-modal/store.ts
index b5acdb6018..bda945819d 100644
--- a/apps/sim/stores/settings-modal/store.ts
+++ b/apps/sim/stores/settings-modal/store.ts
@@ -15,6 +15,7 @@ type SettingsSection =
| 'copilot'
| 'mcp'
| 'custom-tools'
+ | 'workflow-mcp-servers'
interface SettingsModalState {
isOpen: boolean
diff --git a/apps/sim/stores/sidebar/store.ts b/apps/sim/stores/sidebar/store.ts
index 139330eec3..42d13c4365 100644
--- a/apps/sim/stores/sidebar/store.ts
+++ b/apps/sim/stores/sidebar/store.ts
@@ -1,5 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
+import { SIDEBAR_WIDTH } from '@/stores/constants'
/**
* Sidebar state interface
@@ -15,24 +16,17 @@ interface SidebarState {
setHasHydrated: (hasHydrated: boolean) => void
}
-/**
- * Sidebar width constraints
- * Note: Maximum width is enforced dynamically at 30% of viewport width in the resize hook
- */
-export const DEFAULT_SIDEBAR_WIDTH = 232
-export const MIN_SIDEBAR_WIDTH = 232
-
export const useSidebarStore = create()(
persist(
(set, get) => ({
workspaceDropdownOpen: false,
- sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
+ sidebarWidth: SIDEBAR_WIDTH.DEFAULT,
isCollapsed: false,
_hasHydrated: false,
setWorkspaceDropdownOpen: (isOpen) => set({ workspaceDropdownOpen: isOpen }),
setSidebarWidth: (width) => {
// Only enforce minimum - maximum is enforced dynamically by the resize hook
- const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, width)
+ const clampedWidth = Math.max(SIDEBAR_WIDTH.MIN, width)
set({ sidebarWidth: clampedWidth })
// Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') {
diff --git a/apps/sim/stores/terminal/index.ts b/apps/sim/stores/terminal/index.ts
index fc88ce281e..c4449d1470 100644
--- a/apps/sim/stores/terminal/index.ts
+++ b/apps/sim/stores/terminal/index.ts
@@ -1,3 +1,3 @@
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console'
export { useTerminalConsoleStore } from './console'
-export { DEFAULT_TERMINAL_HEIGHT, MIN_TERMINAL_HEIGHT, useTerminalStore } from './store'
+export { useTerminalStore } from './store'
diff --git a/apps/sim/stores/terminal/store.ts b/apps/sim/stores/terminal/store.ts
index c4f31c5b11..218b95310f 100644
--- a/apps/sim/stores/terminal/store.ts
+++ b/apps/sim/stores/terminal/store.ts
@@ -1,5 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
+import { OUTPUT_PANEL_WIDTH, TERMINAL_HEIGHT } from '@/stores/constants'
/**
* Display mode type for terminal output.
@@ -41,22 +42,6 @@ interface TerminalState {
setHasHydrated: (hasHydrated: boolean) => void
}
-/**
- * Terminal height constraints.
- *
- * @remarks
- * The maximum height is enforced dynamically at 70% of the viewport height
- * inside the resize hook to keep the workflow canvas visible.
- */
-export const MIN_TERMINAL_HEIGHT = 30
-export const DEFAULT_TERMINAL_HEIGHT = 196
-
-/**
- * Output panel width constraints.
- */
-const MIN_OUTPUT_PANEL_WIDTH = 440
-const DEFAULT_OUTPUT_PANEL_WIDTH = 440
-
/**
* Default display mode for terminal output.
*/
@@ -65,8 +50,8 @@ const DEFAULT_OUTPUT_PANEL_WIDTH = 440
export const useTerminalStore = create()(
persist(
(set) => ({
- terminalHeight: DEFAULT_TERMINAL_HEIGHT,
- lastExpandedHeight: DEFAULT_TERMINAL_HEIGHT,
+ terminalHeight: TERMINAL_HEIGHT.DEFAULT,
+ lastExpandedHeight: TERMINAL_HEIGHT.DEFAULT,
isResizing: false,
/**
* Updates the terminal height and synchronizes the CSS custom property.
@@ -79,12 +64,12 @@ export const useTerminalStore = create()(
* @param height - Desired terminal height in pixels.
*/
setTerminalHeight: (height) => {
- const clampedHeight = Math.max(MIN_TERMINAL_HEIGHT, height)
+ const clampedHeight = Math.max(TERMINAL_HEIGHT.MIN, height)
set((state) => ({
terminalHeight: clampedHeight,
lastExpandedHeight:
- clampedHeight > MIN_TERMINAL_HEIGHT ? clampedHeight : state.lastExpandedHeight,
+ clampedHeight > TERMINAL_HEIGHT.MIN ? clampedHeight : state.lastExpandedHeight,
}))
// Update CSS variable for immediate visual feedback
@@ -100,14 +85,14 @@ export const useTerminalStore = create()(
setIsResizing: (isResizing) => {
set({ isResizing })
},
- outputPanelWidth: DEFAULT_OUTPUT_PANEL_WIDTH,
+ outputPanelWidth: OUTPUT_PANEL_WIDTH.DEFAULT,
/**
* Updates the output panel width, enforcing the minimum constraint.
*
* @param width - Desired width in pixels for the output panel.
*/
setOutputPanelWidth: (width) => {
- const clampedWidth = Math.max(MIN_OUTPUT_PANEL_WIDTH, width)
+ const clampedWidth = Math.max(OUTPUT_PANEL_WIDTH.MIN, width)
set({ outputPanelWidth: clampedWidth })
},
openOnRun: true,
diff --git a/apps/sim/tools/jsm/add_comment.ts b/apps/sim/tools/jsm/add_comment.ts
new file mode 100644
index 0000000000..daba0098a8
--- /dev/null
+++ b/apps/sim/tools/jsm/add_comment.ts
@@ -0,0 +1,116 @@
+import type { JsmAddCommentParams, JsmAddCommentResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmAddCommentTool: ToolConfig = {
+ id: 'jsm_add_comment',
+ name: 'JSM Add Comment',
+ description: 'Add a comment (public or internal) to a service request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ body: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Comment body text',
+ },
+ isPublic: {
+ type: 'boolean',
+ required: true,
+ visibility: 'user-only',
+ description: 'Whether the comment is public (visible to customer) or internal',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/comment',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ body: params.body,
+ isPublic: params.isPublic,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ commentId: '',
+ body: '',
+ isPublic: false,
+ success: false,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ commentId: '',
+ body: '',
+ isPublic: false,
+ success: false,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ commentId: { type: 'string', description: 'Created comment ID' },
+ body: { type: 'string', description: 'Comment body text' },
+ isPublic: { type: 'boolean', description: 'Whether the comment is public' },
+ success: { type: 'boolean', description: 'Whether the comment was added successfully' },
+ },
+}
diff --git a/apps/sim/tools/jsm/add_customer.ts b/apps/sim/tools/jsm/add_customer.ts
new file mode 100644
index 0000000000..f7440860f1
--- /dev/null
+++ b/apps/sim/tools/jsm/add_customer.ts
@@ -0,0 +1,100 @@
+import type { JsmAddCustomerParams, JsmAddCustomerResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmAddCustomerTool: ToolConfig = {
+ id: 'jsm_add_customer',
+ name: 'JSM Add Customer',
+ description: 'Add customers to a service desk in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ serviceDeskId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Service Desk ID to add customers to',
+ },
+ emails: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Comma-separated email addresses to add as customers',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/customers',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ serviceDeskId: params.serviceDeskId,
+ emails: params.emails,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ serviceDeskId: '',
+ success: false,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ serviceDeskId: '',
+ success: false,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ serviceDeskId: { type: 'string', description: 'Service desk ID' },
+ success: { type: 'boolean', description: 'Whether customers were added successfully' },
+ },
+}
diff --git a/apps/sim/tools/jsm/add_organization_to_service_desk.ts b/apps/sim/tools/jsm/add_organization_to_service_desk.ts
new file mode 100644
index 0000000000..b4004da7dd
--- /dev/null
+++ b/apps/sim/tools/jsm/add_organization_to_service_desk.ts
@@ -0,0 +1,110 @@
+import type {
+ JsmAddOrganizationToServiceDeskParams,
+ JsmAddOrganizationToServiceDeskResponse,
+} from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmAddOrganizationToServiceDeskTool: ToolConfig<
+ JsmAddOrganizationToServiceDeskParams,
+ JsmAddOrganizationToServiceDeskResponse
+> = {
+ id: 'jsm_add_organization_to_service_desk',
+ name: 'JSM Add Organization to Service Desk',
+ description: 'Add an organization to a service desk in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ serviceDeskId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Service Desk ID to add the organization to',
+ },
+ organizationId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Organization ID to add to the service desk',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/organization',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ serviceDeskId: params.serviceDeskId,
+ organizationId: params.organizationId,
+ action: 'add_to_service_desk',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ serviceDeskId: '',
+ organizationId: '',
+ success: false,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ serviceDeskId: '',
+ organizationId: '',
+ success: false,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ serviceDeskId: { type: 'string', description: 'Service Desk ID' },
+ organizationId: { type: 'string', description: 'Organization ID added' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded' },
+ },
+}
diff --git a/apps/sim/tools/jsm/add_participants.ts b/apps/sim/tools/jsm/add_participants.ts
new file mode 100644
index 0000000000..ea32b399f7
--- /dev/null
+++ b/apps/sim/tools/jsm/add_participants.ts
@@ -0,0 +1,107 @@
+import type { JsmAddParticipantsParams, JsmAddParticipantsResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmAddParticipantsTool: ToolConfig<
+ JsmAddParticipantsParams,
+ JsmAddParticipantsResponse
+> = {
+ id: 'jsm_add_participants',
+ name: 'JSM Add Participants',
+ description: 'Add participants to a request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ accountIds: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Comma-separated account IDs to add as participants',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/participants',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ accountIds: params.accountIds,
+ action: 'add',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ participants: [],
+ success: false,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ participants: [],
+ success: false,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ participants: { type: 'json', description: 'Array of added participants' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded' },
+ },
+}
diff --git a/apps/sim/tools/jsm/answer_approval.ts b/apps/sim/tools/jsm/answer_approval.ts
new file mode 100644
index 0000000000..5a9b780b0c
--- /dev/null
+++ b/apps/sim/tools/jsm/answer_approval.ts
@@ -0,0 +1,115 @@
+import type { JsmAnswerApprovalParams, JsmAnswerApprovalResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmAnswerApprovalTool: ToolConfig =
+ {
+ id: 'jsm_answer_approval',
+ name: 'JSM Answer Approval',
+ description: 'Approve or decline an approval request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ approvalId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Approval ID to answer',
+ },
+ decision: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Decision: "approve" or "decline"',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/approvals',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ approvalId: params.approvalId,
+ decision: params.decision,
+ action: 'answer',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ approvalId: '',
+ decision: '',
+ success: false,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ approvalId: '',
+ decision: '',
+ success: false,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ approvalId: { type: 'string', description: 'Approval ID' },
+ decision: { type: 'string', description: 'Decision made (approve/decline)' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded' },
+ },
+ }
diff --git a/apps/sim/tools/jsm/create_organization.ts b/apps/sim/tools/jsm/create_organization.ts
new file mode 100644
index 0000000000..8b3b4617fc
--- /dev/null
+++ b/apps/sim/tools/jsm/create_organization.ts
@@ -0,0 +1,100 @@
+import type { JsmCreateOrganizationParams, JsmCreateOrganizationResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmCreateOrganizationTool: ToolConfig<
+ JsmCreateOrganizationParams,
+ JsmCreateOrganizationResponse
+> = {
+ id: 'jsm_create_organization',
+ name: 'JSM Create Organization',
+ description: 'Create a new organization in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ name: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Name of the organization to create',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/organization',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ name: params.name,
+ action: 'create',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ organizationId: '',
+ name: '',
+ success: false,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ organizationId: '',
+ name: '',
+ success: false,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ organizationId: { type: 'string', description: 'ID of the created organization' },
+ name: { type: 'string', description: 'Name of the created organization' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded' },
+ },
+}
diff --git a/apps/sim/tools/jsm/create_request.ts b/apps/sim/tools/jsm/create_request.ts
new file mode 100644
index 0000000000..9be85017e1
--- /dev/null
+++ b/apps/sim/tools/jsm/create_request.ts
@@ -0,0 +1,134 @@
+import type { JsmCreateRequestParams, JsmCreateRequestResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmCreateRequestTool: ToolConfig = {
+ id: 'jsm_create_request',
+ name: 'JSM Create Request',
+ description: 'Create a new service request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ serviceDeskId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Service Desk ID to create the request in',
+ },
+ requestTypeId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Request Type ID for the new request',
+ },
+ summary: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Summary/title for the service request',
+ },
+ description: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Description for the service request',
+ },
+ raiseOnBehalfOf: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Account ID of customer to raise request on behalf of',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/request',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ serviceDeskId: params.serviceDeskId,
+ requestTypeId: params.requestTypeId,
+ summary: params.summary,
+ description: params.description,
+ raiseOnBehalfOf: params.raiseOnBehalfOf,
+ requestFieldValues: params.requestFieldValues,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueId: '',
+ issueKey: '',
+ requestTypeId: '',
+ serviceDeskId: '',
+ success: false,
+ url: '',
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueId: '',
+ issueKey: '',
+ requestTypeId: '',
+ serviceDeskId: '',
+ success: false,
+ url: '',
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueId: { type: 'string', description: 'Created request issue ID' },
+ issueKey: { type: 'string', description: 'Created request issue key (e.g., SD-123)' },
+ requestTypeId: { type: 'string', description: 'Request type ID' },
+ serviceDeskId: { type: 'string', description: 'Service desk ID' },
+ success: { type: 'boolean', description: 'Whether the request was created successfully' },
+ url: { type: 'string', description: 'URL to the created request' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_approvals.ts b/apps/sim/tools/jsm/get_approvals.ts
new file mode 100644
index 0000000000..d73ab4cd56
--- /dev/null
+++ b/apps/sim/tools/jsm/get_approvals.ts
@@ -0,0 +1,114 @@
+import type { JsmGetApprovalsParams, JsmGetApprovalsResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetApprovalsTool: ToolConfig = {
+ id: 'jsm_get_approvals',
+ name: 'JSM Get Approvals',
+ description: 'Get approvals for a request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/approvals',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ start: params.start,
+ limit: params.limit,
+ action: 'get',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ approvals: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ approvals: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ approvals: { type: 'json', description: 'Array of approvals' },
+ total: { type: 'number', description: 'Total number of approvals' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_comments.ts b/apps/sim/tools/jsm/get_comments.ts
new file mode 100644
index 0000000000..b5322b6c1c
--- /dev/null
+++ b/apps/sim/tools/jsm/get_comments.ts
@@ -0,0 +1,127 @@
+import type { JsmGetCommentsParams, JsmGetCommentsResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetCommentsTool: ToolConfig = {
+ id: 'jsm_get_comments',
+ name: 'JSM Get Comments',
+ description: 'Get comments for a service request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ isPublic: {
+ type: 'boolean',
+ required: false,
+ visibility: 'user-only',
+ description: 'Filter to only public comments',
+ },
+ internal: {
+ type: 'boolean',
+ required: false,
+ visibility: 'user-only',
+ description: 'Filter to only internal comments',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/comments',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ isPublic: params.isPublic,
+ internal: params.internal,
+ start: params.start,
+ limit: params.limit,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ comments: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ comments: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ comments: { type: 'json', description: 'Array of comments' },
+ total: { type: 'number', description: 'Total number of comments' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_customers.ts b/apps/sim/tools/jsm/get_customers.ts
new file mode 100644
index 0000000000..3557fd497e
--- /dev/null
+++ b/apps/sim/tools/jsm/get_customers.ts
@@ -0,0 +1,117 @@
+import type { JsmGetCustomersParams, JsmGetCustomersResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetCustomersTool: ToolConfig = {
+ id: 'jsm_get_customers',
+ name: 'JSM Get Customers',
+ description: 'Get customers for a service desk in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ serviceDeskId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Service Desk ID to get customers for',
+ },
+ query: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Search query to filter customers',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/customers',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ serviceDeskId: params.serviceDeskId,
+ query: params.query,
+ start: params.start,
+ limit: params.limit,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ customers: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ customers: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ customers: { type: 'json', description: 'Array of customers' },
+ total: { type: 'number', description: 'Total number of customers' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_organizations.ts b/apps/sim/tools/jsm/get_organizations.ts
new file mode 100644
index 0000000000..d99b800747
--- /dev/null
+++ b/apps/sim/tools/jsm/get_organizations.ts
@@ -0,0 +1,113 @@
+import type { JsmGetOrganizationsParams, JsmGetOrganizationsResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetOrganizationsTool: ToolConfig<
+ JsmGetOrganizationsParams,
+ JsmGetOrganizationsResponse
+> = {
+ id: 'jsm_get_organizations',
+ name: 'JSM Get Organizations',
+ description: 'Get organizations for a service desk in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ serviceDeskId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Service Desk ID to get organizations for',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/organizations',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ serviceDeskId: params.serviceDeskId,
+ start: params.start,
+ limit: params.limit,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ organizations: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ organizations: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ organizations: { type: 'json', description: 'Array of organizations' },
+ total: { type: 'number', description: 'Total number of organizations' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_participants.ts b/apps/sim/tools/jsm/get_participants.ts
new file mode 100644
index 0000000000..665264f674
--- /dev/null
+++ b/apps/sim/tools/jsm/get_participants.ts
@@ -0,0 +1,117 @@
+import type { JsmGetParticipantsParams, JsmGetParticipantsResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetParticipantsTool: ToolConfig<
+ JsmGetParticipantsParams,
+ JsmGetParticipantsResponse
+> = {
+ id: 'jsm_get_participants',
+ name: 'JSM Get Participants',
+ description: 'Get participants for a request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/participants',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ start: params.start,
+ limit: params.limit,
+ action: 'get',
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ participants: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ participants: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ participants: { type: 'json', description: 'Array of participants' },
+ total: { type: 'number', description: 'Total number of participants' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_queues.ts b/apps/sim/tools/jsm/get_queues.ts
new file mode 100644
index 0000000000..37b26bb768
--- /dev/null
+++ b/apps/sim/tools/jsm/get_queues.ts
@@ -0,0 +1,117 @@
+import type { JsmGetQueuesParams, JsmGetQueuesResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetQueuesTool: ToolConfig = {
+ id: 'jsm_get_queues',
+ name: 'JSM Get Queues',
+ description: 'Get queues for a service desk in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ serviceDeskId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Service Desk ID to get queues for',
+ },
+ includeCount: {
+ type: 'boolean',
+ required: false,
+ visibility: 'user-only',
+ description: 'Include issue count for each queue',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/queues',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ serviceDeskId: params.serviceDeskId,
+ includeCount: params.includeCount,
+ start: params.start,
+ limit: params.limit,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ queues: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ queues: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ queues: { type: 'json', description: 'Array of queues' },
+ total: { type: 'number', description: 'Total number of queues' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_request.ts b/apps/sim/tools/jsm/get_request.ts
new file mode 100644
index 0000000000..adc328b077
--- /dev/null
+++ b/apps/sim/tools/jsm/get_request.ts
@@ -0,0 +1,90 @@
+import type { JsmGetRequestParams, JsmGetRequestResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetRequestTool: ToolConfig = {
+ id: 'jsm_get_request',
+ name: 'JSM Get Request',
+ description: 'Get a single service request from Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/request',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ request: null,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ request: null,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ request: { type: 'json', description: 'The service request object' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_request_types.ts b/apps/sim/tools/jsm/get_request_types.ts
new file mode 100644
index 0000000000..eff6465c5e
--- /dev/null
+++ b/apps/sim/tools/jsm/get_request_types.ts
@@ -0,0 +1,113 @@
+import type { JsmGetRequestTypesParams, JsmGetRequestTypesResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetRequestTypesTool: ToolConfig<
+ JsmGetRequestTypesParams,
+ JsmGetRequestTypesResponse
+> = {
+ id: 'jsm_get_request_types',
+ name: 'JSM Get Request Types',
+ description: 'Get request types for a service desk in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ serviceDeskId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Service Desk ID to get request types for',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/requesttypes',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ serviceDeskId: params.serviceDeskId,
+ start: params.start,
+ limit: params.limit,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ requestTypes: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ requestTypes: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ requestTypes: { type: 'json', description: 'Array of request types' },
+ total: { type: 'number', description: 'Total number of request types' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_requests.ts b/apps/sim/tools/jsm/get_requests.ts
new file mode 100644
index 0000000000..3814673793
--- /dev/null
+++ b/apps/sim/tools/jsm/get_requests.ts
@@ -0,0 +1,132 @@
+import type { JsmGetRequestsParams, JsmGetRequestsResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetRequestsTool: ToolConfig = {
+ id: 'jsm_get_requests',
+ name: 'JSM Get Requests',
+ description: 'Get multiple service requests from Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ serviceDeskId: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Filter by service desk ID',
+ },
+ requestOwnership: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description:
+ 'Filter by ownership: OWNED_REQUESTS, PARTICIPATED_REQUESTS, ORGANIZATION, ALL_REQUESTS',
+ },
+ requestStatus: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Filter by status: OPEN, CLOSED, ALL',
+ },
+ searchTerm: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Search term to filter requests',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/requests',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ serviceDeskId: params.serviceDeskId,
+ requestOwnership: params.requestOwnership,
+ requestStatus: params.requestStatus,
+ searchTerm: params.searchTerm,
+ start: params.start,
+ limit: params.limit,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ requests: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ requests: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ requests: { type: 'json', description: 'Array of service requests' },
+ total: { type: 'number', description: 'Total number of requests' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_service_desks.ts b/apps/sim/tools/jsm/get_service_desks.ts
new file mode 100644
index 0000000000..4e4d3a801b
--- /dev/null
+++ b/apps/sim/tools/jsm/get_service_desks.ts
@@ -0,0 +1,106 @@
+import type { JsmGetServiceDesksParams, JsmGetServiceDesksResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetServiceDesksTool: ToolConfig<
+ JsmGetServiceDesksParams,
+ JsmGetServiceDesksResponse
+> = {
+ id: 'jsm_get_service_desks',
+ name: 'JSM Get Service Desks',
+ description: 'Get all service desks from Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/servicedesks',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ start: params.start,
+ limit: params.limit,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ serviceDesks: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ serviceDesks: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ serviceDesks: { type: 'json', description: 'Array of service desks' },
+ total: { type: 'number', description: 'Total number of service desks' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_sla.ts b/apps/sim/tools/jsm/get_sla.ts
new file mode 100644
index 0000000000..e345598479
--- /dev/null
+++ b/apps/sim/tools/jsm/get_sla.ts
@@ -0,0 +1,113 @@
+import type { JsmGetSlaParams, JsmGetSlaResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetSlaTool: ToolConfig = {
+ id: 'jsm_get_sla',
+ name: 'JSM Get SLA',
+ description: 'Get SLA information for a service request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ start: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Start index for pagination (default: 0)',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-only',
+ description: 'Maximum results to return (default: 50)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/sla',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ start: params.start,
+ limit: params.limit,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ slas: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ slas: [],
+ total: 0,
+ isLastPage: true,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ slas: { type: 'json', description: 'Array of SLA information' },
+ total: { type: 'number', description: 'Total number of SLAs' },
+ isLastPage: { type: 'boolean', description: 'Whether this is the last page' },
+ },
+}
diff --git a/apps/sim/tools/jsm/get_transitions.ts b/apps/sim/tools/jsm/get_transitions.ts
new file mode 100644
index 0000000000..41a61f4821
--- /dev/null
+++ b/apps/sim/tools/jsm/get_transitions.ts
@@ -0,0 +1,94 @@
+import type { JsmGetTransitionsParams, JsmGetTransitionsResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmGetTransitionsTool: ToolConfig =
+ {
+ id: 'jsm_get_transitions',
+ name: 'JSM Get Transitions',
+ description: 'Get available transitions for a service request in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/transitions',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ transitions: [],
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ transitions: [],
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ transitions: { type: 'json', description: 'Array of available transitions' },
+ },
+ }
diff --git a/apps/sim/tools/jsm/index.ts b/apps/sim/tools/jsm/index.ts
new file mode 100644
index 0000000000..b8deb9162f
--- /dev/null
+++ b/apps/sim/tools/jsm/index.ts
@@ -0,0 +1,43 @@
+import { jsmAddCommentTool } from '@/tools/jsm/add_comment'
+import { jsmAddCustomerTool } from '@/tools/jsm/add_customer'
+import { jsmAddOrganizationToServiceDeskTool } from '@/tools/jsm/add_organization_to_service_desk'
+import { jsmAddParticipantsTool } from '@/tools/jsm/add_participants'
+import { jsmAnswerApprovalTool } from '@/tools/jsm/answer_approval'
+import { jsmCreateOrganizationTool } from '@/tools/jsm/create_organization'
+import { jsmCreateRequestTool } from '@/tools/jsm/create_request'
+import { jsmGetApprovalsTool } from '@/tools/jsm/get_approvals'
+import { jsmGetCommentsTool } from '@/tools/jsm/get_comments'
+import { jsmGetCustomersTool } from '@/tools/jsm/get_customers'
+import { jsmGetOrganizationsTool } from '@/tools/jsm/get_organizations'
+import { jsmGetParticipantsTool } from '@/tools/jsm/get_participants'
+import { jsmGetQueuesTool } from '@/tools/jsm/get_queues'
+import { jsmGetRequestTool } from '@/tools/jsm/get_request'
+import { jsmGetRequestTypesTool } from '@/tools/jsm/get_request_types'
+import { jsmGetRequestsTool } from '@/tools/jsm/get_requests'
+import { jsmGetServiceDesksTool } from '@/tools/jsm/get_service_desks'
+import { jsmGetSlaTool } from '@/tools/jsm/get_sla'
+import { jsmGetTransitionsTool } from '@/tools/jsm/get_transitions'
+import { jsmTransitionRequestTool } from '@/tools/jsm/transition_request'
+
+export {
+ jsmAddCommentTool,
+ jsmAddCustomerTool,
+ jsmAddOrganizationToServiceDeskTool,
+ jsmAddParticipantsTool,
+ jsmAnswerApprovalTool,
+ jsmCreateOrganizationTool,
+ jsmCreateRequestTool,
+ jsmGetApprovalsTool,
+ jsmGetCommentsTool,
+ jsmGetCustomersTool,
+ jsmGetOrganizationsTool,
+ jsmGetParticipantsTool,
+ jsmGetQueuesTool,
+ jsmGetRequestTool,
+ jsmGetRequestsTool,
+ jsmGetRequestTypesTool,
+ jsmGetServiceDesksTool,
+ jsmGetSlaTool,
+ jsmGetTransitionsTool,
+ jsmTransitionRequestTool,
+}
diff --git a/apps/sim/tools/jsm/transition_request.ts b/apps/sim/tools/jsm/transition_request.ts
new file mode 100644
index 0000000000..fb2232f6c6
--- /dev/null
+++ b/apps/sim/tools/jsm/transition_request.ts
@@ -0,0 +1,113 @@
+import type { JsmTransitionRequestParams, JsmTransitionRequestResponse } from '@/tools/jsm/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const jsmTransitionRequestTool: ToolConfig<
+ JsmTransitionRequestParams,
+ JsmTransitionRequestResponse
+> = {
+ id: 'jsm_transition_request',
+ name: 'JSM Transition Request',
+ description: 'Transition a service request to a new status in Jira Service Management',
+ version: '1.0.0',
+
+ oauth: {
+ required: true,
+ provider: 'jira',
+ },
+
+ params: {
+ accessToken: {
+ type: 'string',
+ required: true,
+ visibility: 'hidden',
+ description: 'OAuth access token for Jira Service Management',
+ },
+ domain: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
+ },
+ cloudId: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Jira Cloud ID for the instance',
+ },
+ issueIdOrKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Issue ID or key (e.g., SD-123)',
+ },
+ transitionId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Transition ID to apply',
+ },
+ comment: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Optional comment to add during transition',
+ },
+ },
+
+ request: {
+ url: '/api/tools/jsm/transition',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ domain: params.domain,
+ accessToken: params.accessToken,
+ cloudId: params.cloudId,
+ issueIdOrKey: params.issueIdOrKey,
+ transitionId: params.transitionId,
+ comment: params.comment,
+ }),
+ },
+
+ transformResponse: async (response: Response) => {
+ const responseText = await response.text()
+
+ if (!responseText) {
+ return {
+ success: false,
+ output: {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ transitionId: '',
+ success: false,
+ },
+ error: 'Empty response from API',
+ }
+ }
+
+ const data = JSON.parse(responseText)
+
+ if (data.success && data.output) {
+ return data
+ }
+
+ return {
+ success: data.success || false,
+ output: data.output || {
+ ts: new Date().toISOString(),
+ issueIdOrKey: '',
+ transitionId: '',
+ success: false,
+ },
+ error: data.error,
+ }
+ },
+
+ outputs: {
+ ts: { type: 'string', description: 'Timestamp of the operation' },
+ issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
+ transitionId: { type: 'string', description: 'Applied transition ID' },
+ success: { type: 'boolean', description: 'Whether the transition was successful' },
+ },
+}
diff --git a/apps/sim/tools/jsm/types.ts b/apps/sim/tools/jsm/types.ts
new file mode 100644
index 0000000000..591d54b4c7
--- /dev/null
+++ b/apps/sim/tools/jsm/types.ts
@@ -0,0 +1,469 @@
+import type { ToolResponse } from '@/tools/types'
+
+/** Common parameters for all JSM API calls */
+export interface JsmBaseParams {
+ accessToken: string
+ domain: string
+ cloudId?: string
+}
+
+/** Service Desk representation */
+export interface JsmServiceDesk {
+ id: string
+ projectId: string
+ projectName: string
+ projectKey: string
+}
+
+/** Request Type representation */
+export interface JsmRequestType {
+ id: string
+ name: string
+ description: string
+ helpText?: string
+ serviceDeskId: string
+ groupIds: string[]
+ icon: {
+ id: string
+ name: string
+ }
+}
+
+/** Customer representation */
+export interface JsmCustomer {
+ accountId: string
+ name: string
+ key: string
+ emailAddress: string
+ displayName: string
+ active: boolean
+ timeZone: string
+}
+
+/** Organization representation */
+export interface JsmOrganization {
+ id: string
+ name: string
+}
+
+/** Queue representation */
+export interface JsmQueue {
+ id: string
+ name: string
+ jql: string
+ fields: string[]
+ issueCount: number
+}
+
+/** SLA representation */
+export interface JsmSla {
+ id: string
+ name: string
+ completedCycles: Array<{
+ startTime: { iso8601: string }
+ stopTime: { iso8601: string }
+ breached: boolean
+ }>
+ ongoingCycle?: {
+ startTime: { iso8601: string }
+ breachTime?: { iso8601: string }
+ breached: boolean
+ paused: boolean
+ withinCalendarHours: boolean
+ goalDuration?: { millis: number; friendly: string }
+ elapsedTime?: { millis: number; friendly: string }
+ remainingTime?: { millis: number; friendly: string }
+ }
+}
+
+/** Request (ticket) representation */
+export interface JsmRequest {
+ issueId: string
+ issueKey: string
+ requestTypeId: string
+ serviceDeskId: string
+ createdDate: { iso8601: string; friendly: string }
+ reporter: JsmCustomer
+ requestFieldValues: Array<{
+ fieldId: string
+ label: string
+ value: unknown
+ }>
+ currentStatus: {
+ status: string
+ statusCategory: string
+ statusDate: { iso8601: string; friendly: string }
+ }
+}
+
+/** Comment representation */
+export interface JsmComment {
+ id: string
+ body: string
+ public: boolean
+ author: {
+ accountId: string
+ displayName: string
+ emailAddress?: string
+ }
+ created: { iso8601: string; friendly: string }
+}
+
+/** Transition representation */
+export interface JsmTransition {
+ id: string
+ name: string
+}
+
+export interface JsmGetServiceDesksParams extends JsmBaseParams {
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetServiceDesksResponse extends ToolResponse {
+ output: {
+ ts: string
+ serviceDesks: JsmServiceDesk[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmGetRequestTypesParams extends JsmBaseParams {
+ serviceDeskId: string
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetRequestTypesResponse extends ToolResponse {
+ output: {
+ ts: string
+ requestTypes: JsmRequestType[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmCreateRequestParams extends JsmBaseParams {
+ serviceDeskId: string
+ requestTypeId: string
+ summary: string
+ description?: string
+ requestFieldValues?: Record
+ raiseOnBehalfOf?: string
+}
+
+export interface JsmCreateRequestResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueId: string
+ issueKey: string
+ requestTypeId: string
+ serviceDeskId: string
+ success: boolean
+ url: string
+ }
+}
+
+export interface JsmGetRequestParams extends JsmBaseParams {
+ issueIdOrKey: string
+}
+
+export interface JsmGetRequestResponse extends ToolResponse {
+ output: {
+ ts: string
+ request: JsmRequest
+ }
+}
+
+export interface JsmGetRequestsParams extends JsmBaseParams {
+ serviceDeskId?: string
+ requestOwnership?: 'OWNED_REQUESTS' | 'PARTICIPATED_REQUESTS' | 'ORGANIZATION' | 'ALL_REQUESTS'
+ requestStatus?: 'OPEN' | 'CLOSED' | 'ALL'
+ searchTerm?: string
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetRequestsResponse extends ToolResponse {
+ output: {
+ ts: string
+ requests: JsmRequest[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmAddCommentParams extends JsmBaseParams {
+ issueIdOrKey: string
+ body: string
+ isPublic: boolean
+}
+
+export interface JsmAddCommentResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ commentId: string
+ body: string
+ isPublic: boolean
+ success: boolean
+ }
+}
+
+export interface JsmGetCommentsParams extends JsmBaseParams {
+ issueIdOrKey: string
+ isPublic?: boolean
+ internal?: boolean
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetCommentsResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ comments: JsmComment[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmGetCustomersParams extends JsmBaseParams {
+ serviceDeskId: string
+ query?: string
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetCustomersResponse extends ToolResponse {
+ output: {
+ ts: string
+ customers: JsmCustomer[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmAddCustomerParams extends JsmBaseParams {
+ serviceDeskId: string
+ emails: string
+}
+
+export interface JsmAddCustomerResponse extends ToolResponse {
+ output: {
+ ts: string
+ serviceDeskId: string
+ success: boolean
+ }
+}
+
+export interface JsmGetOrganizationsParams extends JsmBaseParams {
+ serviceDeskId: string
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetOrganizationsResponse extends ToolResponse {
+ output: {
+ ts: string
+ organizations: JsmOrganization[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmGetQueuesParams extends JsmBaseParams {
+ serviceDeskId: string
+ includeCount?: boolean
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetQueuesResponse extends ToolResponse {
+ output: {
+ ts: string
+ queues: JsmQueue[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmGetSlaParams extends JsmBaseParams {
+ issueIdOrKey: string
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetSlaResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ slas: JsmSla[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmTransitionRequestParams extends JsmBaseParams {
+ issueIdOrKey: string
+ transitionId: string
+ comment?: string
+}
+
+export interface JsmTransitionRequestResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ transitionId: string
+ success: boolean
+ }
+}
+
+export interface JsmGetTransitionsParams extends JsmBaseParams {
+ issueIdOrKey: string
+}
+
+export interface JsmGetTransitionsResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ transitions: JsmTransition[]
+ }
+}
+
+export interface JsmCreateOrganizationParams extends JsmBaseParams {
+ name: string
+}
+
+export interface JsmCreateOrganizationResponse extends ToolResponse {
+ output: {
+ ts: string
+ organizationId: string
+ name: string
+ success: boolean
+ }
+}
+
+export interface JsmAddOrganizationToServiceDeskParams extends JsmBaseParams {
+ serviceDeskId: string
+ organizationId: string
+}
+
+export interface JsmAddOrganizationToServiceDeskResponse extends ToolResponse {
+ output: {
+ ts: string
+ serviceDeskId: string
+ organizationId: string
+ success: boolean
+ }
+}
+
+export interface JsmParticipant {
+ accountId: string
+ displayName: string
+ emailAddress?: string
+ active: boolean
+}
+
+export interface JsmGetParticipantsParams extends JsmBaseParams {
+ issueIdOrKey: string
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetParticipantsResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ participants: JsmParticipant[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmAddParticipantsParams extends JsmBaseParams {
+ issueIdOrKey: string
+ accountIds: string
+}
+
+export interface JsmAddParticipantsResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ participants: JsmParticipant[]
+ success: boolean
+ }
+}
+
+export interface JsmApprover {
+ accountId: string
+ displayName: string
+ emailAddress?: string
+ approverDecision: 'pending' | 'approved' | 'declined'
+}
+
+export interface JsmApproval {
+ id: string
+ name: string
+ finalDecision: 'pending' | 'approved' | 'declined'
+ canAnswerApproval: boolean
+ approvers: JsmApprover[]
+ createdDate?: { iso8601: string; friendly: string }
+ completedDate?: { iso8601: string; friendly: string }
+}
+
+export interface JsmGetApprovalsParams extends JsmBaseParams {
+ issueIdOrKey: string
+ start?: number
+ limit?: number
+}
+
+export interface JsmGetApprovalsResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ approvals: JsmApproval[]
+ total: number
+ isLastPage: boolean
+ }
+}
+
+export interface JsmAnswerApprovalParams extends JsmBaseParams {
+ issueIdOrKey: string
+ approvalId: string
+ decision: 'approve' | 'decline'
+}
+
+export interface JsmAnswerApprovalResponse extends ToolResponse {
+ output: {
+ ts: string
+ issueIdOrKey: string
+ approvalId: string
+ decision: string
+ success: boolean
+ }
+}
+
+/** Union type for all JSM responses */
+export type JsmResponse =
+ | JsmGetServiceDesksResponse
+ | JsmGetRequestTypesResponse
+ | JsmCreateRequestResponse
+ | JsmGetRequestResponse
+ | JsmGetRequestsResponse
+ | JsmAddCommentResponse
+ | JsmGetCommentsResponse
+ | JsmGetCustomersResponse
+ | JsmAddCustomerResponse
+ | JsmGetOrganizationsResponse
+ | JsmGetQueuesResponse
+ | JsmGetSlaResponse
+ | JsmTransitionRequestResponse
+ | JsmGetTransitionsResponse
+ | JsmCreateOrganizationResponse
+ | JsmAddOrganizationToServiceDeskResponse
+ | JsmGetParticipantsResponse
+ | JsmAddParticipantsResponse
+ | JsmGetApprovalsResponse
+ | JsmAnswerApprovalResponse
diff --git a/apps/sim/tools/jsm/utils.ts b/apps/sim/tools/jsm/utils.ts
new file mode 100644
index 0000000000..b523e6ba2c
--- /dev/null
+++ b/apps/sim/tools/jsm/utils.ts
@@ -0,0 +1,28 @@
+/**
+ * Shared utilities for Jira Service Management tools
+ * Reuses the getJiraCloudId from the Jira integration since JSM uses the same Atlassian Cloud ID
+ */
+export { getJiraCloudId } from '@/tools/jira/utils'
+
+/**
+ * Build the base URL for JSM Service Desk API
+ * @param cloudId - The Jira Cloud ID
+ * @returns The base URL for the Service Desk API
+ */
+export function getJsmApiBaseUrl(cloudId: string): string {
+ return `https://api.atlassian.com/ex/jira/${cloudId}/rest/servicedeskapi`
+}
+
+/**
+ * Build common headers for JSM API requests
+ * @param accessToken - The OAuth access token
+ * @returns Headers object for API requests
+ */
+export function getJsmHeaders(accessToken: string): Record {
+ return {
+ Authorization: `Bearer ${accessToken}`,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'X-ExperimentalApi': 'opt-in',
+ }
+}
diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts
index 4610808eb3..61d1df6435 100644
--- a/apps/sim/tools/registry.ts
+++ b/apps/sim/tools/registry.ts
@@ -491,6 +491,28 @@ import {
jiraUpdateWorklogTool,
jiraWriteTool,
} from '@/tools/jira'
+import {
+ jsmAddCommentTool,
+ jsmAddCustomerTool,
+ jsmAddOrganizationToServiceDeskTool,
+ jsmAddParticipantsTool,
+ jsmAnswerApprovalTool,
+ jsmCreateOrganizationTool,
+ jsmCreateRequestTool,
+ jsmGetApprovalsTool,
+ jsmGetCommentsTool,
+ jsmGetCustomersTool,
+ jsmGetOrganizationsTool,
+ jsmGetParticipantsTool,
+ jsmGetQueuesTool,
+ jsmGetRequestsTool,
+ jsmGetRequestTool,
+ jsmGetRequestTypesTool,
+ jsmGetServiceDesksTool,
+ jsmGetSlaTool,
+ jsmGetTransitionsTool,
+ jsmTransitionRequestTool,
+} from '@/tools/jsm'
import {
kalshiAmendOrderTool,
kalshiCancelOrderTool,
@@ -1498,6 +1520,26 @@ export const tools: Record = {
jira_add_watcher: jiraAddWatcherTool,
jira_remove_watcher: jiraRemoveWatcherTool,
jira_get_users: jiraGetUsersTool,
+ jsm_get_service_desks: jsmGetServiceDesksTool,
+ jsm_get_request_types: jsmGetRequestTypesTool,
+ jsm_create_request: jsmCreateRequestTool,
+ jsm_get_request: jsmGetRequestTool,
+ jsm_get_requests: jsmGetRequestsTool,
+ jsm_add_comment: jsmAddCommentTool,
+ jsm_get_comments: jsmGetCommentsTool,
+ jsm_get_customers: jsmGetCustomersTool,
+ jsm_add_customer: jsmAddCustomerTool,
+ jsm_get_organizations: jsmGetOrganizationsTool,
+ jsm_create_organization: jsmCreateOrganizationTool,
+ jsm_add_organization_to_service_desk: jsmAddOrganizationToServiceDeskTool,
+ jsm_get_queues: jsmGetQueuesTool,
+ jsm_get_sla: jsmGetSlaTool,
+ jsm_get_transitions: jsmGetTransitionsTool,
+ jsm_transition_request: jsmTransitionRequestTool,
+ jsm_get_participants: jsmGetParticipantsTool,
+ jsm_add_participants: jsmAddParticipantsTool,
+ jsm_get_approvals: jsmGetApprovalsTool,
+ jsm_answer_approval: jsmAnswerApprovalTool,
kalshi_get_markets: kalshiGetMarketsTool,
kalshi_get_market: kalshiGetMarketTool,
kalshi_get_events: kalshiGetEventsTool,
diff --git a/bun.lock b/bun.lock
index 5d8d14d070..23513563b2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,6 +1,6 @@
{
"lockfileVersion": 1,
- "configVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
@@ -127,7 +127,6 @@
"ffmpeg-static": "5.3.0",
"fluent-ffmpeg": "2.1.3",
"framer-motion": "^12.5.0",
- "fuse.js": "7.1.0",
"google-auth-library": "10.5.0",
"gray-matter": "^4.0.3",
"groq-sdk": "^0.15.0",
@@ -2177,8 +2176,6 @@
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
- "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
-
"gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile
index c94c1d9257..f1ee3cf8a0 100644
--- a/docker/app.Dockerfile
+++ b/docker/app.Dockerfile
@@ -26,11 +26,12 @@ COPY packages/logger/package.json ./packages/logger/package.json
COPY packages/tsconfig/package.json ./packages/tsconfig/package.json
# Install turbo globally, then dependencies, then rebuild isolated-vm for Node.js
+# Use --linker=hoisted for flat node_modules layout (required for Docker multi-stage builds)
RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \
--mount=type=cache,id=npm-cache,target=/root/.npm \
bun install -g turbo && \
- HUSKY=0 bun install --omit=dev --ignore-scripts && \
- cd node_modules/.bun/isolated-vm@*/node_modules/isolated-vm && npx node-gyp rebuild --release && cd /app
+ HUSKY=0 bun install --omit=dev --ignore-scripts --linker=hoisted && \
+ cd node_modules/isolated-vm && npx node-gyp rebuild --release
# ========================================
# Builder Stage: Build the Application
@@ -65,7 +66,7 @@ COPY packages ./packages
# Required for standalone nextjs build
WORKDIR /app/apps/sim
RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \
- HUSKY=0 bun install sharp
+ HUSKY=0 bun install sharp --linker=hoisted
ENV NEXT_TELEMETRY_DISABLED=1 \
VERCEL_TELEMETRY_DISABLED=1 \
@@ -105,7 +106,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/.next/static ./apps/sim/.next/static
# Copy isolated-vm native module (compiled for Node.js in deps stage)
-COPY --from=deps --chown=nextjs:nodejs /app/node_modules/.bun/isolated-vm@6.0.2/node_modules/isolated-vm ./node_modules/isolated-vm
+COPY --from=deps --chown=nextjs:nodejs /app/node_modules/isolated-vm ./node_modules/isolated-vm
# Copy the isolated-vm worker script
COPY --from=builder --chown=nextjs:nodejs /app/apps/sim/lib/execution/isolated-vm-worker.cjs ./apps/sim/lib/execution/isolated-vm-worker.cjs
diff --git a/docker/realtime.Dockerfile b/docker/realtime.Dockerfile
index d5ebaffec5..01994521e0 100644
--- a/docker/realtime.Dockerfile
+++ b/docker/realtime.Dockerfile
@@ -18,9 +18,10 @@ COPY packages/testing/package.json ./packages/testing/package.json
COPY packages/logger/package.json ./packages/logger/package.json
COPY packages/tsconfig/package.json ./packages/tsconfig/package.json
-# Install dependencies with cache mount for faster builds
+# Install dependencies with hoisted layout for Docker compatibility
+# Using --linker=hoisted to avoid .bun directory symlinks that don't copy between stages
RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \
- bun install --omit=dev --ignore-scripts
+ bun install --omit=dev --ignore-scripts --linker=hoisted
# ========================================
# Builder Stage: Prepare source code
diff --git a/helm/sim/templates/networkpolicy.yaml b/helm/sim/templates/networkpolicy.yaml
index deac5a5dba..7ef8697417 100644
--- a/helm/sim/templates/networkpolicy.yaml
+++ b/helm/sim/templates/networkpolicy.yaml
@@ -141,6 +141,10 @@ spec:
ports:
- protocol: TCP
port: 443
+ # Allow custom egress rules
+ {{- with .Values.networkPolicy.egress }}
+ {{- toYaml . | nindent 2 }}
+ {{- end }}
{{- end }}
{{- if .Values.postgresql.enabled }}
diff --git a/packages/db/migrations/0134_parallel_galactus.sql b/packages/db/migrations/0134_parallel_galactus.sql
new file mode 100644
index 0000000000..fc5330f356
--- /dev/null
+++ b/packages/db/migrations/0134_parallel_galactus.sql
@@ -0,0 +1,30 @@
+CREATE TABLE "workflow_mcp_server" (
+ "id" text PRIMARY KEY NOT NULL,
+ "workspace_id" text NOT NULL,
+ "created_by" text NOT NULL,
+ "name" text NOT NULL,
+ "description" text,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "workflow_mcp_tool" (
+ "id" text PRIMARY KEY NOT NULL,
+ "server_id" text NOT NULL,
+ "workflow_id" text NOT NULL,
+ "tool_name" text NOT NULL,
+ "tool_description" text,
+ "parameter_schema" json DEFAULT '{}' NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "workflow_mcp_server" ADD CONSTRAINT "workflow_mcp_server_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "workflow_mcp_server" ADD CONSTRAINT "workflow_mcp_server_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "workflow_mcp_tool" ADD CONSTRAINT "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk" FOREIGN KEY ("server_id") REFERENCES "public"."workflow_mcp_server"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "workflow_mcp_tool" ADD CONSTRAINT "workflow_mcp_tool_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "workflow_mcp_server_workspace_id_idx" ON "workflow_mcp_server" USING btree ("workspace_id");--> statement-breakpoint
+CREATE INDEX "workflow_mcp_server_created_by_idx" ON "workflow_mcp_server" USING btree ("created_by");--> statement-breakpoint
+CREATE INDEX "workflow_mcp_tool_server_id_idx" ON "workflow_mcp_tool" USING btree ("server_id");--> statement-breakpoint
+CREATE INDEX "workflow_mcp_tool_workflow_id_idx" ON "workflow_mcp_tool" USING btree ("workflow_id");--> statement-breakpoint
+CREATE UNIQUE INDEX "workflow_mcp_tool_server_workflow_unique" ON "workflow_mcp_tool" USING btree ("server_id","workflow_id");
\ No newline at end of file
diff --git a/packages/db/migrations/meta/0134_snapshot.json b/packages/db/migrations/meta/0134_snapshot.json
new file mode 100644
index 0000000000..51abc68936
--- /dev/null
+++ b/packages/db/migrations/meta/0134_snapshot.json
@@ -0,0 +1,8813 @@
+{
+ "id": "30899c79-8b72-46e7-a7b0-d9fe760c59ea",
+ "prevId": "d0a70259-bb0b-4aaf-99ce-806343b6fa81",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "account_user_id_idx": {
+ "name": "account_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_account_on_account_id_provider_id": {
+ "name": "idx_account_on_account_id_provider_id",
+ "columns": [
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "account_user_provider_account_unique": {
+ "name": "account_user_provider_account_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_key": {
+ "name": "api_key",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'personal'"
+ },
+ "last_used": {
+ "name": "last_used",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "api_key_workspace_type_idx": {
+ "name": "api_key_workspace_type_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "api_key_user_type_idx": {
+ "name": "api_key_user_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "api_key_user_id_user_id_fk": {
+ "name": "api_key_user_id_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "api_key_workspace_id_workspace_id_fk": {
+ "name": "api_key_workspace_id_workspace_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "api_key_created_by_user_id_fk": {
+ "name": "api_key_created_by_user_id_fk",
+ "tableFrom": "api_key",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_key_key_unique": {
+ "name": "api_key_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "workspace_type_check": {
+ "name": "workspace_type_check",
+ "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.chat": {
+ "name": "chat",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "customizations": {
+ "name": "customizations",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'public'"
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowed_emails": {
+ "name": "allowed_emails",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "output_configs": {
+ "name": "output_configs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "identifier_idx": {
+ "name": "identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_workflow_id_workflow_id_fk": {
+ "name": "chat_workflow_id_workflow_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_user_id_user_id_fk": {
+ "name": "chat_user_id_user_id_fk",
+ "tableFrom": "chat",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_chats": {
+ "name": "copilot_chats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "messages": {
+ "name": "messages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'claude-3-7-sonnet-latest'"
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preview_yaml": {
+ "name": "preview_yaml",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "plan_artifact": {
+ "name": "plan_artifact",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_chats_user_id_idx": {
+ "name": "copilot_chats_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_workflow_id_idx": {
+ "name": "copilot_chats_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_workflow_idx": {
+ "name": "copilot_chats_user_workflow_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_created_at_idx": {
+ "name": "copilot_chats_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_updated_at_idx": {
+ "name": "copilot_chats_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_chats_user_id_user_id_fk": {
+ "name": "copilot_chats_user_id_user_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_chats_workflow_id_workflow_id_fk": {
+ "name": "copilot_chats_workflow_id_workflow_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_feedback": {
+ "name": "copilot_feedback",
+ "schema": "",
+ "columns": {
+ "feedback_id": {
+ "name": "feedback_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_query": {
+ "name": "user_query",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_response": {
+ "name": "agent_response",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_positive": {
+ "name": "is_positive",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feedback": {
+ "name": "feedback",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_yaml": {
+ "name": "workflow_yaml",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "copilot_feedback_user_id_idx": {
+ "name": "copilot_feedback_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_chat_id_idx": {
+ "name": "copilot_feedback_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_user_chat_idx": {
+ "name": "copilot_feedback_user_chat_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_is_positive_idx": {
+ "name": "copilot_feedback_is_positive_idx",
+ "columns": [
+ {
+ "expression": "is_positive",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_feedback_created_at_idx": {
+ "name": "copilot_feedback_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_feedback_user_id_user_id_fk": {
+ "name": "copilot_feedback_user_id_user_id_fk",
+ "tableFrom": "copilot_feedback",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_feedback_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_feedback_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_feedback",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom_tools": {
+ "name": "custom_tools",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema": {
+ "name": "schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "custom_tools_workspace_id_idx": {
+ "name": "custom_tools_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_tools_workspace_title_unique": {
+ "name": "custom_tools_workspace_title_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "custom_tools_workspace_id_workspace_id_fk": {
+ "name": "custom_tools_workspace_id_workspace_id_fk",
+ "tableFrom": "custom_tools",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "custom_tools_user_id_user_id_fk": {
+ "name": "custom_tools_user_id_user_id_fk",
+ "tableFrom": "custom_tools",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.docs_embeddings": {
+ "name": "docs_embeddings",
+ "schema": "",
+ "columns": {
+ "chunk_id": {
+ "name": "chunk_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "chunk_text": {
+ "name": "chunk_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_document": {
+ "name": "source_document",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_link": {
+ "name": "source_link",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "header_text": {
+ "name": "header_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "header_level": {
+ "name": "header_level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "chunk_text_tsv": {
+ "name": "chunk_text_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "docs_emb_source_document_idx": {
+ "name": "docs_emb_source_document_idx",
+ "columns": [
+ {
+ "expression": "source_document",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_header_level_idx": {
+ "name": "docs_emb_header_level_idx",
+ "columns": [
+ {
+ "expression": "header_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_source_header_idx": {
+ "name": "docs_emb_source_header_idx",
+ "columns": [
+ {
+ "expression": "source_document",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "header_level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_model_idx": {
+ "name": "docs_emb_model_idx",
+ "columns": [
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_emb_created_at_idx": {
+ "name": "docs_emb_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "docs_embedding_vector_hnsw_idx": {
+ "name": "docs_embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "docs_emb_metadata_gin_idx": {
+ "name": "docs_emb_metadata_gin_idx",
+ "columns": [
+ {
+ "expression": "metadata",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "docs_emb_chunk_text_fts_idx": {
+ "name": "docs_emb_chunk_text_fts_idx",
+ "columns": [
+ {
+ "expression": "chunk_text_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "docs_embedding_not_null_check": {
+ "name": "docs_embedding_not_null_check",
+ "value": "\"embedding\" IS NOT NULL"
+ },
+ "docs_header_level_check": {
+ "name": "docs_header_level_check",
+ "value": "\"header_level\" >= 1 AND \"header_level\" <= 6"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.document": {
+ "name": "document",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_count": {
+ "name": "chunk_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "character_count": {
+ "name": "character_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "processing_status": {
+ "name": "processing_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_completed_at": {
+ "name": "processing_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_error": {
+ "name": "processing_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag1": {
+ "name": "tag1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag2": {
+ "name": "tag2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag3": {
+ "name": "tag3",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag4": {
+ "name": "tag4",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag5": {
+ "name": "tag5",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag6": {
+ "name": "tag6",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag7": {
+ "name": "tag7",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number1": {
+ "name": "number1",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number2": {
+ "name": "number2",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number3": {
+ "name": "number3",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number4": {
+ "name": "number4",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number5": {
+ "name": "number5",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date1": {
+ "name": "date1",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date2": {
+ "name": "date2",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean1": {
+ "name": "boolean1",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean2": {
+ "name": "boolean2",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean3": {
+ "name": "boolean3",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "doc_kb_id_idx": {
+ "name": "doc_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_filename_idx": {
+ "name": "doc_filename_idx",
+ "columns": [
+ {
+ "expression": "filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_processing_status_idx": {
+ "name": "doc_processing_status_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "processing_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag1_idx": {
+ "name": "doc_tag1_idx",
+ "columns": [
+ {
+ "expression": "tag1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag2_idx": {
+ "name": "doc_tag2_idx",
+ "columns": [
+ {
+ "expression": "tag2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag3_idx": {
+ "name": "doc_tag3_idx",
+ "columns": [
+ {
+ "expression": "tag3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag4_idx": {
+ "name": "doc_tag4_idx",
+ "columns": [
+ {
+ "expression": "tag4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag5_idx": {
+ "name": "doc_tag5_idx",
+ "columns": [
+ {
+ "expression": "tag5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag6_idx": {
+ "name": "doc_tag6_idx",
+ "columns": [
+ {
+ "expression": "tag6",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_tag7_idx": {
+ "name": "doc_tag7_idx",
+ "columns": [
+ {
+ "expression": "tag7",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number1_idx": {
+ "name": "doc_number1_idx",
+ "columns": [
+ {
+ "expression": "number1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number2_idx": {
+ "name": "doc_number2_idx",
+ "columns": [
+ {
+ "expression": "number2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number3_idx": {
+ "name": "doc_number3_idx",
+ "columns": [
+ {
+ "expression": "number3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number4_idx": {
+ "name": "doc_number4_idx",
+ "columns": [
+ {
+ "expression": "number4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_number5_idx": {
+ "name": "doc_number5_idx",
+ "columns": [
+ {
+ "expression": "number5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_date1_idx": {
+ "name": "doc_date1_idx",
+ "columns": [
+ {
+ "expression": "date1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_date2_idx": {
+ "name": "doc_date2_idx",
+ "columns": [
+ {
+ "expression": "date2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean1_idx": {
+ "name": "doc_boolean1_idx",
+ "columns": [
+ {
+ "expression": "boolean1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean2_idx": {
+ "name": "doc_boolean2_idx",
+ "columns": [
+ {
+ "expression": "boolean2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_boolean3_idx": {
+ "name": "doc_boolean3_idx",
+ "columns": [
+ {
+ "expression": "boolean3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "document_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "document_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "document",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.embedding": {
+ "name": "embedding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_index": {
+ "name": "chunk_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chunk_hash": {
+ "name": "chunk_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_length": {
+ "name": "content_length",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(1536)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag1": {
+ "name": "tag1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag2": {
+ "name": "tag2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag3": {
+ "name": "tag3",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag4": {
+ "name": "tag4",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag5": {
+ "name": "tag5",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag6": {
+ "name": "tag6",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tag7": {
+ "name": "tag7",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number1": {
+ "name": "number1",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number2": {
+ "name": "number2",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number3": {
+ "name": "number3",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number4": {
+ "name": "number4",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "number5": {
+ "name": "number5",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date1": {
+ "name": "date1",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "date2": {
+ "name": "date2",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean1": {
+ "name": "boolean1",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean2": {
+ "name": "boolean2",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "boolean3": {
+ "name": "boolean3",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "content_tsv": {
+ "name": "content_tsv",
+ "type": "tsvector",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "to_tsvector('english', \"embedding\".\"content\")",
+ "type": "stored"
+ }
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "emb_kb_id_idx": {
+ "name": "emb_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_id_idx": {
+ "name": "emb_doc_id_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_chunk_idx": {
+ "name": "emb_doc_chunk_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chunk_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_model_idx": {
+ "name": "emb_kb_model_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "embedding_model",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_enabled_idx": {
+ "name": "emb_kb_enabled_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_doc_enabled_idx": {
+ "name": "emb_doc_enabled_idx",
+ "columns": [
+ {
+ "expression": "document_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "embedding_vector_hnsw_idx": {
+ "name": "embedding_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "emb_tag1_idx": {
+ "name": "emb_tag1_idx",
+ "columns": [
+ {
+ "expression": "tag1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag2_idx": {
+ "name": "emb_tag2_idx",
+ "columns": [
+ {
+ "expression": "tag2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag3_idx": {
+ "name": "emb_tag3_idx",
+ "columns": [
+ {
+ "expression": "tag3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag4_idx": {
+ "name": "emb_tag4_idx",
+ "columns": [
+ {
+ "expression": "tag4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag5_idx": {
+ "name": "emb_tag5_idx",
+ "columns": [
+ {
+ "expression": "tag5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag6_idx": {
+ "name": "emb_tag6_idx",
+ "columns": [
+ {
+ "expression": "tag6",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_tag7_idx": {
+ "name": "emb_tag7_idx",
+ "columns": [
+ {
+ "expression": "tag7",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number1_idx": {
+ "name": "emb_number1_idx",
+ "columns": [
+ {
+ "expression": "number1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number2_idx": {
+ "name": "emb_number2_idx",
+ "columns": [
+ {
+ "expression": "number2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number3_idx": {
+ "name": "emb_number3_idx",
+ "columns": [
+ {
+ "expression": "number3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number4_idx": {
+ "name": "emb_number4_idx",
+ "columns": [
+ {
+ "expression": "number4",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_number5_idx": {
+ "name": "emb_number5_idx",
+ "columns": [
+ {
+ "expression": "number5",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_date1_idx": {
+ "name": "emb_date1_idx",
+ "columns": [
+ {
+ "expression": "date1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_date2_idx": {
+ "name": "emb_date2_idx",
+ "columns": [
+ {
+ "expression": "date2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean1_idx": {
+ "name": "emb_boolean1_idx",
+ "columns": [
+ {
+ "expression": "boolean1",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean2_idx": {
+ "name": "emb_boolean2_idx",
+ "columns": [
+ {
+ "expression": "boolean2",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_boolean3_idx": {
+ "name": "emb_boolean3_idx",
+ "columns": [
+ {
+ "expression": "boolean3",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_content_fts_idx": {
+ "name": "emb_content_fts_idx",
+ "columns": [
+ {
+ "expression": "content_tsv",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "embedding_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "embedding_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "embedding_document_id_document_id_fk": {
+ "name": "embedding_document_id_document_id_fk",
+ "tableFrom": "embedding",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "embedding_not_null_check": {
+ "name": "embedding_not_null_check",
+ "value": "\"embedding\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.environment": {
+ "name": "environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "environment_user_id_user_id_fk": {
+ "name": "environment_user_id_user_id_fk",
+ "tableFrom": "environment",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "environment_user_id_unique": {
+ "name": "environment_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.idempotency_key": {
+ "name": "idempotency_key",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "namespace": {
+ "name": "namespace",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "result": {
+ "name": "result",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idempotency_key_namespace_unique": {
+ "name": "idempotency_key_namespace_unique",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "namespace",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idempotency_key_created_at_idx": {
+ "name": "idempotency_key_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idempotency_key_namespace_idx": {
+ "name": "idempotency_key_namespace_idx",
+ "columns": [
+ {
+ "expression": "namespace",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation": {
+ "name": "invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "invitation_email_idx": {
+ "name": "invitation_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_organization_id_idx": {
+ "name": "invitation_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base": {
+ "name": "knowledge_base",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "embedding_model": {
+ "name": "embedding_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text-embedding-3-small'"
+ },
+ "embedding_dimension": {
+ "name": "embedding_dimension",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1536
+ },
+ "chunking_config": {
+ "name": "chunking_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_user_id_idx": {
+ "name": "kb_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_id_idx": {
+ "name": "kb_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_user_workspace_idx": {
+ "name": "kb_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_deleted_at_idx": {
+ "name": "kb_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_user_id_user_id_fk": {
+ "name": "knowledge_base_user_id_user_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_workspace_id_workspace_id_fk": {
+ "name": "knowledge_base_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_base_tag_definitions": {
+ "name": "knowledge_base_tag_definitions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_slot": {
+ "name": "tag_slot",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_type": {
+ "name": "field_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'text'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kb_tag_definitions_kb_slot_idx": {
+ "name": "kb_tag_definitions_kb_slot_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tag_slot",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_tag_definitions_kb_display_name_idx": {
+ "name": "kb_tag_definitions_kb_display_name_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_tag_definitions_kb_id_idx": {
+ "name": "kb_tag_definitions_kb_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "knowledge_base_tag_definitions",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_servers": {
+ "name": "mcp_servers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "transport": {
+ "name": "transport",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "headers": {
+ "name": "headers",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "timeout": {
+ "name": "timeout",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 30000
+ },
+ "retries": {
+ "name": "retries",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_connected": {
+ "name": "last_connected",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "connection_status": {
+ "name": "connection_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'disconnected'"
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_config": {
+ "name": "status_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "tool_count": {
+ "name": "tool_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_tools_refresh": {
+ "name": "last_tools_refresh",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_requests": {
+ "name": "total_requests",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_used": {
+ "name": "last_used",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mcp_servers_workspace_enabled_idx": {
+ "name": "mcp_servers_workspace_enabled_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_servers_workspace_deleted_idx": {
+ "name": "mcp_servers_workspace_deleted_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_servers_workspace_id_workspace_id_fk": {
+ "name": "mcp_servers_workspace_id_workspace_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_servers_created_by_user_id_fk": {
+ "name": "mcp_servers_created_by_user_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.member": {
+ "name": "member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "member_user_id_idx": {
+ "name": "member_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "member_organization_id_idx": {
+ "name": "member_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.memory": {
+ "name": "memory",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "memory_key_idx": {
+ "name": "memory_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workspace_idx": {
+ "name": "memory_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "memory_workspace_key_idx": {
+ "name": "memory_workspace_key_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "memory_workspace_id_workspace_id_fk": {
+ "name": "memory_workspace_id_workspace_id_fk",
+ "tableFrom": "memory",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization": {
+ "name": "organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "org_usage_limit": {
+ "name": "org_usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "storage_used_bytes": {
+ "name": "storage_used_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "departed_member_usage": {
+ "name": "departed_member_usage",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "credit_balance": {
+ "name": "credit_balance",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.paused_executions": {
+ "name": "paused_executions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_snapshot": {
+ "name": "execution_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pause_points": {
+ "name": "pause_points",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_pause_count": {
+ "name": "total_pause_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resumed_count": {
+ "name": "resumed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'paused'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "paused_at": {
+ "name": "paused_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "paused_executions_workflow_id_idx": {
+ "name": "paused_executions_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "paused_executions_status_idx": {
+ "name": "paused_executions_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "paused_executions_execution_id_unique": {
+ "name": "paused_executions_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "paused_executions_workflow_id_workflow_id_fk": {
+ "name": "paused_executions_workflow_id_workflow_id_fk",
+ "tableFrom": "paused_executions",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.permissions": {
+ "name": "permissions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permissions_user_id_idx": {
+ "name": "permissions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_entity_idx": {
+ "name": "permissions_entity_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_type_idx": {
+ "name": "permissions_user_entity_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_permission_idx": {
+ "name": "permissions_user_entity_permission_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "permission_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_user_entity_idx": {
+ "name": "permissions_user_entity_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permissions_unique_constraint": {
+ "name": "permissions_unique_constraint",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permissions_user_id_user_id_fk": {
+ "name": "permissions_user_id_user_id_fk",
+ "tableFrom": "permissions",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rate_limit_bucket": {
+ "name": "rate_limit_bucket",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tokens": {
+ "name": "tokens",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_refill_at": {
+ "name": "last_refill_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.resume_queue": {
+ "name": "resume_queue",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "paused_execution_id": {
+ "name": "paused_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_execution_id": {
+ "name": "parent_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "new_execution_id": {
+ "name": "new_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "context_id": {
+ "name": "context_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resume_input": {
+ "name": "resume_input",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "queued_at": {
+ "name": "queued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "claimed_at": {
+ "name": "claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_reason": {
+ "name": "failure_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "resume_queue_parent_status_idx": {
+ "name": "resume_queue_parent_status_idx",
+ "columns": [
+ {
+ "expression": "parent_execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "queued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "resume_queue_new_execution_idx": {
+ "name": "resume_queue_new_execution_idx",
+ "columns": [
+ {
+ "expression": "new_execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "resume_queue_paused_execution_id_paused_executions_id_fk": {
+ "name": "resume_queue_paused_execution_id_paused_executions_id_fk",
+ "tableFrom": "resume_queue",
+ "tableTo": "paused_executions",
+ "columnsFrom": ["paused_execution_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "session_user_id_idx": {
+ "name": "session_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "session_token_idx": {
+ "name": "session_token_idx",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_active_organization_id_organization_id_fk": {
+ "name": "session_active_organization_id_organization_id_fk",
+ "tableFrom": "session",
+ "tableTo": "organization",
+ "columnsFrom": ["active_organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.settings": {
+ "name": "settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "theme": {
+ "name": "theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'system'"
+ },
+ "auto_connect": {
+ "name": "auto_connect",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "telemetry_enabled": {
+ "name": "telemetry_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "email_preferences": {
+ "name": "email_preferences",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "billing_usage_notifications_enabled": {
+ "name": "billing_usage_notifications_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "show_training_controls": {
+ "name": "show_training_controls",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "super_user_mode_enabled": {
+ "name": "super_user_mode_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "error_notifications_enabled": {
+ "name": "error_notifications_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "snap_to_grid_size": {
+ "name": "snap_to_grid_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "copilot_enabled_models": {
+ "name": "copilot_enabled_models",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "copilot_auto_allowed_tools": {
+ "name": "copilot_auto_allowed_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "settings_user_id_user_id_fk": {
+ "name": "settings_user_id_user_id_fk",
+ "tableFrom": "settings",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "settings_user_id_unique": {
+ "name": "settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sso_provider": {
+ "name": "sso_provider",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_config": {
+ "name": "oidc_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "saml_config": {
+ "name": "saml_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "sso_provider_provider_id_idx": {
+ "name": "sso_provider_provider_id_idx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_domain_idx": {
+ "name": "sso_provider_domain_idx",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_user_id_idx": {
+ "name": "sso_provider_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_provider_organization_id_idx": {
+ "name": "sso_provider_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sso_provider_user_id_user_id_fk": {
+ "name": "sso_provider_user_id_user_id_fk",
+ "tableFrom": "sso_provider",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "sso_provider_organization_id_organization_id_fk": {
+ "name": "sso_provider_organization_id_organization_id_fk",
+ "tableFrom": "sso_provider",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.subscription": {
+ "name": "subscription",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "plan": {
+ "name": "plan",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_start": {
+ "name": "period_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "period_end": {
+ "name": "period_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_at_period_end": {
+ "name": "cancel_at_period_end",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seats": {
+ "name": "seats",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_start": {
+ "name": "trial_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trial_end": {
+ "name": "trial_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "subscription_reference_status_idx": {
+ "name": "subscription_reference_status_idx",
+ "columns": [
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "check_enterprise_metadata": {
+ "name": "check_enterprise_metadata",
+ "value": "plan != 'enterprise' OR metadata IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.template_creators": {
+ "name": "template_creators",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "reference_type": {
+ "name": "reference_type",
+ "type": "template_creator_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_image_url": {
+ "name": "profile_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "verified": {
+ "name": "verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "template_creators_reference_idx": {
+ "name": "template_creators_reference_idx",
+ "columns": [
+ {
+ "expression": "reference_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_creators_reference_id_idx": {
+ "name": "template_creators_reference_id_idx",
+ "columns": [
+ {
+ "expression": "reference_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_creators_created_by_idx": {
+ "name": "template_creators_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "template_creators_created_by_user_id_fk": {
+ "name": "template_creators_created_by_user_id_fk",
+ "tableFrom": "template_creators",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.template_stars": {
+ "name": "template_stars",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "starred_at": {
+ "name": "starred_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "template_stars_user_id_idx": {
+ "name": "template_stars_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_id_idx": {
+ "name": "template_stars_template_id_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_user_template_idx": {
+ "name": "template_stars_user_template_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_user_idx": {
+ "name": "template_stars_template_user_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_starred_at_idx": {
+ "name": "template_stars_starred_at_idx",
+ "columns": [
+ {
+ "expression": "starred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_template_starred_at_idx": {
+ "name": "template_stars_template_starred_at_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "starred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "template_stars_user_template_unique": {
+ "name": "template_stars_user_template_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "template_stars_user_id_user_id_fk": {
+ "name": "template_stars_user_id_user_id_fk",
+ "tableFrom": "template_stars",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "template_stars_template_id_templates_id_fk": {
+ "name": "template_stars_template_id_templates_id_fk",
+ "tableFrom": "template_stars",
+ "tableTo": "templates",
+ "columnsFrom": ["template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.templates": {
+ "name": "templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "views": {
+ "name": "views",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "stars": {
+ "name": "stars",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "template_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "required_credentials": {
+ "name": "required_credentials",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "state": {
+ "name": "state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "og_image_url": {
+ "name": "og_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "templates_status_idx": {
+ "name": "templates_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_creator_id_idx": {
+ "name": "templates_creator_id_idx",
+ "columns": [
+ {
+ "expression": "creator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_views_idx": {
+ "name": "templates_views_idx",
+ "columns": [
+ {
+ "expression": "views",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_stars_idx": {
+ "name": "templates_stars_idx",
+ "columns": [
+ {
+ "expression": "stars",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_status_views_idx": {
+ "name": "templates_status_views_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "views",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_status_stars_idx": {
+ "name": "templates_status_stars_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stars",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_created_at_idx": {
+ "name": "templates_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "templates_updated_at_idx": {
+ "name": "templates_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "templates_workflow_id_workflow_id_fk": {
+ "name": "templates_workflow_id_workflow_id_fk",
+ "tableFrom": "templates",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "templates_creator_id_template_creators_id_fk": {
+ "name": "templates_creator_id_template_creators_id_fk",
+ "tableFrom": "templates",
+ "tableTo": "template_creators",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.usage_log": {
+ "name": "usage_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "usage_log_category",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "usage_log_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cost": {
+ "name": "cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "usage_log_user_created_at_idx": {
+ "name": "usage_log_user_created_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_source_idx": {
+ "name": "usage_log_source_idx",
+ "columns": [
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_workspace_id_idx": {
+ "name": "usage_log_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_workflow_id_idx": {
+ "name": "usage_log_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "usage_log_user_id_user_id_fk": {
+ "name": "usage_log_user_id_user_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "usage_log_workspace_id_workspace_id_fk": {
+ "name": "usage_log_workspace_id_workspace_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "usage_log_workflow_id_workflow_id_fk": {
+ "name": "usage_log_workflow_id_workflow_id_fk",
+ "tableFrom": "usage_log",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_super_user": {
+ "name": "is_super_user",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_stats": {
+ "name": "user_stats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_manual_executions": {
+ "name": "total_manual_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_api_calls": {
+ "name": "total_api_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_webhook_triggers": {
+ "name": "total_webhook_triggers",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_scheduled_executions": {
+ "name": "total_scheduled_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_chat_executions": {
+ "name": "total_chat_executions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_tokens_used": {
+ "name": "total_tokens_used",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_cost": {
+ "name": "total_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "current_usage_limit": {
+ "name": "current_usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'20'"
+ },
+ "usage_limit_updated_at": {
+ "name": "usage_limit_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "current_period_cost": {
+ "name": "current_period_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "last_period_cost": {
+ "name": "last_period_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "billed_overage_this_period": {
+ "name": "billed_overage_this_period",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "pro_period_cost_snapshot": {
+ "name": "pro_period_cost_snapshot",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "credit_balance": {
+ "name": "credit_balance",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "total_copilot_cost": {
+ "name": "total_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "current_period_copilot_cost": {
+ "name": "current_period_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "last_period_copilot_cost": {
+ "name": "last_period_copilot_cost",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0'"
+ },
+ "total_copilot_tokens": {
+ "name": "total_copilot_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_copilot_calls": {
+ "name": "total_copilot_calls",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "storage_used_bytes": {
+ "name": "storage_used_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_active": {
+ "name": "last_active",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "billing_blocked": {
+ "name": "billing_blocked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "billing_blocked_reason": {
+ "name": "billing_blocked_reason",
+ "type": "billing_blocked_reason",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_stats_user_id_user_id_fk": {
+ "name": "user_stats_user_id_user_id_fk",
+ "tableFrom": "user_stats",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_stats_user_id_unique": {
+ "name": "user_stats_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "verification_expires_at_idx": {
+ "name": "verification_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.waitlist": {
+ "name": "waitlist",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "waitlist_email_unique": {
+ "name": "waitlist_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhook": {
+ "name": "webhook",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_config": {
+ "name": "provider_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "path_idx": {
+ "name": "path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_webhook_on_workflow_id_block_id": {
+ "name": "idx_webhook_on_workflow_id_block_id",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "webhook_workflow_id_workflow_id_fk": {
+ "name": "webhook_workflow_id_workflow_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "webhook_block_id_workflow_blocks_id_fk": {
+ "name": "webhook_block_id_workflow_blocks_id_fk",
+ "tableFrom": "webhook",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow": {
+ "name": "workflow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#3972F6'"
+ },
+ "last_synced": {
+ "name": "last_synced",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_deployed": {
+ "name": "is_deployed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "deployed_at": {
+ "name": "deployed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "run_count": {
+ "name": "run_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ }
+ },
+ "indexes": {
+ "workflow_user_id_idx": {
+ "name": "workflow_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_id_idx": {
+ "name": "workflow_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_user_workspace_idx": {
+ "name": "workflow_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_user_id_user_id_fk": {
+ "name": "workflow_user_id_user_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_workspace_id_workspace_id_fk": {
+ "name": "workflow_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_id_workflow_folder_id_fk": {
+ "name": "workflow_folder_id_workflow_folder_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "workflow_folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_blocks": {
+ "name": "workflow_blocks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_x": {
+ "name": "position_x",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_y": {
+ "name": "position_y",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "horizontal_handles": {
+ "name": "horizontal_handles",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_wide": {
+ "name": "is_wide",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "advanced_mode": {
+ "name": "advanced_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "trigger_mode": {
+ "name": "trigger_mode",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'0'"
+ },
+ "sub_blocks": {
+ "name": "sub_blocks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "outputs": {
+ "name": "outputs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_blocks_workflow_id_idx": {
+ "name": "workflow_blocks_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_blocks_type_idx": {
+ "name": "workflow_blocks_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_blocks_workflow_id_workflow_id_fk": {
+ "name": "workflow_blocks_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_blocks",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_checkpoints": {
+ "name": "workflow_checkpoints",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_state": {
+ "name": "workflow_state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_checkpoints_user_id_idx": {
+ "name": "workflow_checkpoints_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_workflow_id_idx": {
+ "name": "workflow_checkpoints_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_chat_id_idx": {
+ "name": "workflow_checkpoints_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_message_id_idx": {
+ "name": "workflow_checkpoints_message_id_idx",
+ "columns": [
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_user_workflow_idx": {
+ "name": "workflow_checkpoints_user_workflow_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_workflow_chat_idx": {
+ "name": "workflow_checkpoints_workflow_chat_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_created_at_idx": {
+ "name": "workflow_checkpoints_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_checkpoints_chat_created_at_idx": {
+ "name": "workflow_checkpoints_chat_created_at_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_checkpoints_user_id_user_id_fk": {
+ "name": "workflow_checkpoints_user_id_user_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_checkpoints_workflow_id_workflow_id_fk": {
+ "name": "workflow_checkpoints_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_checkpoints_chat_id_copilot_chats_id_fk": {
+ "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk",
+ "tableFrom": "workflow_checkpoints",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_deployment_version": {
+ "name": "workflow_deployment_version",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "workflow_deployment_version_workflow_version_unique": {
+ "name": "workflow_deployment_version_workflow_version_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_version_workflow_active_idx": {
+ "name": "workflow_deployment_version_workflow_active_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_version_created_at_idx": {
+ "name": "workflow_deployment_version_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_deployment_version_workflow_id_workflow_id_fk": {
+ "name": "workflow_deployment_version_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_deployment_version",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_edges": {
+ "name": "workflow_edges",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_block_id": {
+ "name": "source_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_block_id": {
+ "name": "target_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_handle": {
+ "name": "source_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_handle": {
+ "name": "target_handle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_edges_workflow_id_idx": {
+ "name": "workflow_edges_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_source_idx": {
+ "name": "workflow_edges_workflow_source_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_edges_workflow_target_idx": {
+ "name": "workflow_edges_workflow_target_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_edges_workflow_id_workflow_id_fk": {
+ "name": "workflow_edges_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_source_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_source_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["source_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_edges_target_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_edges_target_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_edges",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["target_block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_execution_logs": {
+ "name": "workflow_execution_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_snapshot_id": {
+ "name": "state_snapshot_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deployment_version_id": {
+ "name": "deployment_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "level": {
+ "name": "level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_duration_ms": {
+ "name": "total_duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "execution_data": {
+ "name": "execution_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "cost": {
+ "name": "cost",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "files": {
+ "name": "files",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_execution_logs_workflow_id_idx": {
+ "name": "workflow_execution_logs_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_state_snapshot_id_idx": {
+ "name": "workflow_execution_logs_state_snapshot_id_idx",
+ "columns": [
+ {
+ "expression": "state_snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_deployment_version_id_idx": {
+ "name": "workflow_execution_logs_deployment_version_id_idx",
+ "columns": [
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_trigger_idx": {
+ "name": "workflow_execution_logs_trigger_idx",
+ "columns": [
+ {
+ "expression": "trigger",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_level_idx": {
+ "name": "workflow_execution_logs_level_idx",
+ "columns": [
+ {
+ "expression": "level",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_started_at_idx": {
+ "name": "workflow_execution_logs_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_execution_id_unique": {
+ "name": "workflow_execution_logs_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workflow_started_at_idx": {
+ "name": "workflow_execution_logs_workflow_started_at_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workspace_started_at_idx": {
+ "name": "workflow_execution_logs_workspace_started_at_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_execution_logs_workflow_id_workflow_id_fk": {
+ "name": "workflow_execution_logs_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_workspace_id_workspace_id_fk": {
+ "name": "workflow_execution_logs_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": {
+ "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow_execution_snapshots",
+ "columnsFrom": ["state_snapshot_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": {
+ "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "workflow_execution_logs",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["deployment_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_execution_snapshots": {
+ "name": "workflow_execution_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_hash": {
+ "name": "state_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_data": {
+ "name": "state_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_snapshots_workflow_id_idx": {
+ "name": "workflow_snapshots_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_hash_idx": {
+ "name": "workflow_snapshots_hash_idx",
+ "columns": [
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_workflow_hash_idx": {
+ "name": "workflow_snapshots_workflow_hash_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_snapshots_created_at_idx": {
+ "name": "workflow_snapshots_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_execution_snapshots_workflow_id_workflow_id_fk": {
+ "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_execution_snapshots",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_folder": {
+ "name": "workflow_folder",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'#6B7280'"
+ },
+ "is_expanded": {
+ "name": "is_expanded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_folder_user_idx": {
+ "name": "workflow_folder_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_workspace_parent_idx": {
+ "name": "workflow_folder_workspace_parent_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_folder_parent_sort_idx": {
+ "name": "workflow_folder_parent_sort_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_folder_user_id_user_id_fk": {
+ "name": "workflow_folder_user_id_user_id_fk",
+ "tableFrom": "workflow_folder",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_folder_workspace_id_workspace_id_fk": {
+ "name": "workflow_folder_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_folder",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_mcp_server": {
+ "name": "workflow_mcp_server",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_mcp_server_workspace_id_idx": {
+ "name": "workflow_mcp_server_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_server_created_by_idx": {
+ "name": "workflow_mcp_server_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_mcp_server_workspace_id_workspace_id_fk": {
+ "name": "workflow_mcp_server_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_mcp_server",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_mcp_server_created_by_user_id_fk": {
+ "name": "workflow_mcp_server_created_by_user_id_fk",
+ "tableFrom": "workflow_mcp_server",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_mcp_tool": {
+ "name": "workflow_mcp_tool",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "server_id": {
+ "name": "server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_name": {
+ "name": "tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_description": {
+ "name": "tool_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parameter_schema": {
+ "name": "parameter_schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_mcp_tool_server_id_idx": {
+ "name": "workflow_mcp_tool_server_id_idx",
+ "columns": [
+ {
+ "expression": "server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_tool_workflow_id_idx": {
+ "name": "workflow_mcp_tool_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_tool_server_workflow_unique": {
+ "name": "workflow_mcp_tool_server_workflow_unique",
+ "columns": [
+ {
+ "expression": "server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": {
+ "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk",
+ "tableFrom": "workflow_mcp_tool",
+ "tableTo": "workflow_mcp_server",
+ "columnsFrom": ["server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_mcp_tool_workflow_id_workflow_id_fk": {
+ "name": "workflow_mcp_tool_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_mcp_tool",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_schedule": {
+ "name": "workflow_schedule",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_run_at": {
+ "name": "next_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_ran_at": {
+ "name": "last_ran_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_queued_at": {
+ "name": "last_queued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_type": {
+ "name": "trigger_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'UTC'"
+ },
+ "failed_count": {
+ "name": "failed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_failed_at": {
+ "name": "last_failed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_schedule_workflow_block_unique": {
+ "name": "workflow_schedule_workflow_block_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_schedule_workflow_id_workflow_id_fk": {
+ "name": "workflow_schedule_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_block_id_workflow_blocks_id_fk": {
+ "name": "workflow_schedule_block_id_workflow_blocks_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow_blocks",
+ "columnsFrom": ["block_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workflow_subflows": {
+ "name": "workflow_subflows",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workflow_subflows_workflow_id_idx": {
+ "name": "workflow_subflows_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_subflows_workflow_type_idx": {
+ "name": "workflow_subflows_workflow_type_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_subflows_workflow_id_workflow_id_fk": {
+ "name": "workflow_subflows_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_subflows",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace": {
+ "name": "workspace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "billed_account_user_id": {
+ "name": "billed_account_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "allow_personal_api_keys": {
+ "name": "allow_personal_api_keys",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_owner_id_user_id_fk": {
+ "name": "workspace_owner_id_user_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_billed_account_user_id_user_id_fk": {
+ "name": "workspace_billed_account_user_id_user_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "user",
+ "columnsFrom": ["billed_account_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_byok_keys": {
+ "name": "workspace_byok_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_byok_provider_unique": {
+ "name": "workspace_byok_provider_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_byok_workspace_idx": {
+ "name": "workspace_byok_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_byok_keys_workspace_id_workspace_id_fk": {
+ "name": "workspace_byok_keys_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_byok_keys",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_byok_keys_created_by_user_id_fk": {
+ "name": "workspace_byok_keys_created_by_user_id_fk",
+ "tableFrom": "workspace_byok_keys",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_environment": {
+ "name": "workspace_environment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variables": {
+ "name": "variables",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_environment_workspace_unique": {
+ "name": "workspace_environment_workspace_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_environment_workspace_id_workspace_id_fk": {
+ "name": "workspace_environment_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_environment",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file": {
+ "name": "workspace_file",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_file_workspace_id_idx": {
+ "name": "workspace_file_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_key_idx": {
+ "name": "workspace_file_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_workspace_id_workspace_id_fk": {
+ "name": "workspace_file_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_file",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_file_uploaded_by_user_id_fk": {
+ "name": "workspace_file_uploaded_by_user_id_fk",
+ "tableFrom": "workspace_file",
+ "tableTo": "user",
+ "columnsFrom": ["uploaded_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_file_key_unique": {
+ "name": "workspace_file_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_files": {
+ "name": "workspace_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "context": {
+ "name": "context",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_name": {
+ "name": "original_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_files_key_idx": {
+ "name": "workspace_files_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_user_id_idx": {
+ "name": "workspace_files_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_workspace_id_idx": {
+ "name": "workspace_files_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_context_idx": {
+ "name": "workspace_files_context_idx",
+ "columns": [
+ {
+ "expression": "context",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_files_user_id_user_id_fk": {
+ "name": "workspace_files_user_id_user_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_files_workspace_id_workspace_id_fk": {
+ "name": "workspace_files_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_files_key_unique": {
+ "name": "workspace_files_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_invitation": {
+ "name": "workspace_invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "status": {
+ "name": "status",
+ "type": "workspace_invitation_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'admin'"
+ },
+ "org_invitation_id": {
+ "name": "org_invitation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_invitation_workspace_id_workspace_id_fk": {
+ "name": "workspace_invitation_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_invitation",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_invitation_inviter_id_user_id_fk": {
+ "name": "workspace_invitation_inviter_id_user_id_fk",
+ "tableFrom": "workspace_invitation",
+ "tableTo": "user",
+ "columnsFrom": ["inviter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspace_invitation_token_unique": {
+ "name": "workspace_invitation_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_notification_delivery": {
+ "name": "workspace_notification_delivery",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "notification_delivery_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_status": {
+ "name": "response_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_body": {
+ "name": "response_body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_notification_delivery_subscription_id_idx": {
+ "name": "workspace_notification_delivery_subscription_id_idx",
+ "columns": [
+ {
+ "expression": "subscription_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_delivery_execution_id_idx": {
+ "name": "workspace_notification_delivery_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_delivery_status_idx": {
+ "name": "workspace_notification_delivery_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_delivery_next_attempt_idx": {
+ "name": "workspace_notification_delivery_next_attempt_idx",
+ "columns": [
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": {
+ "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk",
+ "tableFrom": "workspace_notification_delivery",
+ "tableTo": "workspace_notification_subscription",
+ "columnsFrom": ["subscription_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_notification_delivery_workflow_id_workflow_id_fk": {
+ "name": "workspace_notification_delivery_workflow_id_workflow_id_fk",
+ "tableFrom": "workspace_notification_delivery",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_notification_subscription": {
+ "name": "workspace_notification_subscription",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "notification_type": {
+ "name": "notification_type",
+ "type": "notification_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_ids": {
+ "name": "workflow_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "all_workflows": {
+ "name": "all_workflows",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "level_filter": {
+ "name": "level_filter",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY['info', 'error']::text[]"
+ },
+ "trigger_filter": {
+ "name": "trigger_filter",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]"
+ },
+ "include_final_output": {
+ "name": "include_final_output",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "include_trace_spans": {
+ "name": "include_trace_spans",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "include_rate_limits": {
+ "name": "include_rate_limits",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "include_usage_data": {
+ "name": "include_usage_data",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "webhook_config": {
+ "name": "webhook_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_recipients": {
+ "name": "email_recipients",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_config": {
+ "name": "slack_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "alert_config": {
+ "name": "alert_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_alert_at": {
+ "name": "last_alert_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_notification_workspace_id_idx": {
+ "name": "workspace_notification_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_active_idx": {
+ "name": "workspace_notification_active_idx",
+ "columns": [
+ {
+ "expression": "active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_notification_type_idx": {
+ "name": "workspace_notification_type_idx",
+ "columns": [
+ {
+ "expression": "notification_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_notification_subscription_workspace_id_workspace_id_fk": {
+ "name": "workspace_notification_subscription_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_notification_subscription",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_notification_subscription_created_by_user_id_fk": {
+ "name": "workspace_notification_subscription_created_by_user_id_fk",
+ "tableFrom": "workspace_notification_subscription",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.billing_blocked_reason": {
+ "name": "billing_blocked_reason",
+ "schema": "public",
+ "values": ["payment_failed", "dispute"]
+ },
+ "public.notification_delivery_status": {
+ "name": "notification_delivery_status",
+ "schema": "public",
+ "values": ["pending", "in_progress", "success", "failed"]
+ },
+ "public.notification_type": {
+ "name": "notification_type",
+ "schema": "public",
+ "values": ["webhook", "email", "slack"]
+ },
+ "public.permission_type": {
+ "name": "permission_type",
+ "schema": "public",
+ "values": ["admin", "write", "read"]
+ },
+ "public.template_creator_type": {
+ "name": "template_creator_type",
+ "schema": "public",
+ "values": ["user", "organization"]
+ },
+ "public.template_status": {
+ "name": "template_status",
+ "schema": "public",
+ "values": ["pending", "approved", "rejected"]
+ },
+ "public.usage_log_category": {
+ "name": "usage_log_category",
+ "schema": "public",
+ "values": ["model", "fixed"]
+ },
+ "public.usage_log_source": {
+ "name": "usage_log_source",
+ "schema": "public",
+ "values": ["workflow", "wand", "copilot"]
+ },
+ "public.workspace_invitation_status": {
+ "name": "workspace_invitation_status",
+ "schema": "public",
+ "values": ["pending", "accepted", "rejected", "cancelled"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json
index a59eabe8c3..26a68ca9ab 100644
--- a/packages/db/migrations/meta/_journal.json
+++ b/packages/db/migrations/meta/_journal.json
@@ -932,6 +932,13 @@
"when": 1766607372265,
"tag": "0133_smiling_cargill",
"breakpoints": true
+ },
+ {
+ "idx": 134,
+ "version": "7",
+ "when": 1766779827389,
+ "tag": "0134_parallel_galactus",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/schema.ts b/packages/db/schema.ts
index 65bd10c9a5..692fb11497 100644
--- a/packages/db/schema.ts
+++ b/packages/db/schema.ts
@@ -1688,7 +1688,61 @@ export const ssoProvider = pgTable(
})
)
-// Usage logging for tracking individual billable operations
+/**
+ * Workflow MCP Servers - User-created MCP servers that expose workflows as tools.
+ * These servers are accessible by external MCP clients via API key authentication.
+ */
+export const workflowMcpServer = pgTable(
+ 'workflow_mcp_server',
+ {
+ id: text('id').primaryKey(),
+ workspaceId: text('workspace_id')
+ .notNull()
+ .references(() => workspace.id, { onDelete: 'cascade' }),
+ createdBy: text('created_by')
+ .notNull()
+ .references(() => user.id, { onDelete: 'cascade' }),
+ name: text('name').notNull(),
+ description: text('description'),
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ },
+ (table) => ({
+ workspaceIdIdx: index('workflow_mcp_server_workspace_id_idx').on(table.workspaceId),
+ createdByIdx: index('workflow_mcp_server_created_by_idx').on(table.createdBy),
+ })
+)
+
+/**
+ * Workflow MCP Tools - Workflows registered as tools within a Workflow MCP Server.
+ * Each tool maps to a deployed workflow's execute endpoint.
+ */
+export const workflowMcpTool = pgTable(
+ 'workflow_mcp_tool',
+ {
+ id: text('id').primaryKey(),
+ serverId: text('server_id')
+ .notNull()
+ .references(() => workflowMcpServer.id, { onDelete: 'cascade' }),
+ workflowId: text('workflow_id')
+ .notNull()
+ .references(() => workflow.id, { onDelete: 'cascade' }),
+ toolName: text('tool_name').notNull(),
+ toolDescription: text('tool_description'),
+ parameterSchema: json('parameter_schema').notNull().default('{}'),
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+ },
+ (table) => ({
+ serverIdIdx: index('workflow_mcp_tool_server_id_idx').on(table.serverId),
+ workflowIdIdx: index('workflow_mcp_tool_workflow_id_idx').on(table.workflowId),
+ serverWorkflowUnique: uniqueIndex('workflow_mcp_tool_server_workflow_unique').on(
+ table.serverId,
+ table.workflowId
+ ),
+ })
+)
+
export const usageLogCategoryEnum = pgEnum('usage_log_category', ['model', 'fixed'])
export const usageLogSourceEnum = pgEnum('usage_log_source', ['workflow', 'wand', 'copilot'])
@@ -1700,38 +1754,26 @@ export const usageLog = pgTable(
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
- // Charge category: 'model' (token-based) or 'fixed' (flat fee)
category: usageLogCategoryEnum('category').notNull(),
- // What generated this charge: 'workflow', 'wand', 'copilot'
source: usageLogSourceEnum('source').notNull(),
- // For model charges: model name (e.g., 'gpt-4o', 'claude-4.5-opus')
- // For fixed charges: charge type (e.g., 'execution_fee', 'search_query')
description: text('description').notNull(),
- // Category-specific metadata (e.g., tokens for 'model' category)
metadata: jsonb('metadata'),
- // Cost in USD
cost: decimal('cost').notNull(),
- // Optional context references
workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'set null' }),
workflowId: text('workflow_id').references(() => workflow.id, { onDelete: 'set null' }),
executionId: text('execution_id'),
- // Timestamp
createdAt: timestamp('created_at').notNull().defaultNow(),
},
(table) => ({
- // Index for querying user's usage history (most common query)
userCreatedAtIdx: index('usage_log_user_created_at_idx').on(table.userId, table.createdAt),
- // Index for filtering by source
sourceIdx: index('usage_log_source_idx').on(table.source),
- // Index for workspace-specific queries
workspaceIdIdx: index('usage_log_workspace_id_idx').on(table.workspaceId),
- // Index for workflow-specific queries
workflowIdIdx: index('usage_log_workflow_id_idx').on(table.workflowId),
})
)