orquestra-mcp 1.26.0 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/server.mjs +173 -16
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.26.0",
4
- "description": "Servidor MCP de Orquestra -- 109 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
3
+ "version": "1.29.0",
4
+ "description": "Servidor MCP de Orquestra -- 115 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "orquestra-mcp": "bin/orquestra-mcp.mjs"
@@ -26,6 +26,6 @@
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
28
  "@modelcontextprotocol/sdk": "^1.12.0",
29
- "zod": "^3.23.8"
29
+ "zod": "^4.6.5"
30
30
  }
31
31
  }
package/src/server.mjs CHANGED
@@ -46,13 +46,15 @@ const PLATFORM_TYPES = ['android', 'ios', 'web', 'desktop-mac', 'desktop-win', '
46
46
  const INFRA_STATUSES = ['operational', 'degraded', 'down']
47
47
  const STAGE_IDS = STAGES.map(s => s.id)
48
48
 
49
- // Adjuntos -- entityType en la tool es singular (task/note/idea/doc, más
50
- // natural para el modelo), la API usa el plural de la colección de
51
- // Firestore (tasks/notes/ideas/docs) -- este mapa traduce entre los dos.
52
- const ATTACHMENT_ENTITY_PATH = { task: 'tasks', note: 'notes', idea: 'ideas', doc: 'docs' }
49
+ // Adjuntos -- entityType en la tool es singular (task/note/idea/doc/content,
50
+ // más natural para el modelo), la API usa el plural de la colección de
51
+ // Firestore (tasks/notes/ideas/docs/content) -- este mapa traduce entre los dos.
52
+ const ATTACHMENT_ENTITY_PATH = { task: 'tasks', note: 'notes', idea: 'ideas', doc: 'docs', content: 'content' }
53
53
  const ATTACHMENT_ENTITY_TYPES = Object.keys(ATTACHMENT_ENTITY_PATH)
54
54
  const ATTACHMENT_IMAGE_MIME_BY_EXT = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' }
55
+ const ATTACHMENT_VIDEO_MIME_BY_EXT = { mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm' }
55
56
  const MAX_ATTACHMENT_IMAGE_BYTES = 15 * 1024 * 1024
57
+ const MAX_ATTACHMENT_VIDEO_BYTES = 500 * 1024 * 1024
56
58
  const MAX_ATTACHMENT_CODE_CHARS = 200_000
57
59
 
58
60
  const SETUP_GUIDE = {
@@ -750,6 +752,84 @@ export function createServer() {
750
752
  }
751
753
  )
752
754
 
755
+ // Crear rama + abrir PR borrador desde una tarea (ORQ-204, 10 oct 2026):
756
+ // requiere que la GitHub App de ese repo tenga permisos de escritura en
757
+ // Contents/Pull requests -- si no, el servidor responde 502/403 con el
758
+ // detalle de GitHub. "Crear rama" guarda el nombre en task.branch (lo que
759
+ // ya lee sync_task_from_github); "Abrir PR" exige que la tarea ya tenga
760
+ // branch (de este tool o puesto a mano).
761
+ server.registerTool(
762
+ 'create_task_branch',
763
+ {
764
+ description: 'Crea una rama en GitHub para esta tarea (ej. "ORQ-123-arregla-el-login") desde la rama base de la integración, y la guarda en task.branch. Idempotente: si la rama ya existe, no falla. Si el proyecto tiene varios repos conectados, pasa integrationId (list_github_integrations).',
765
+ inputSchema: {
766
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
767
+ taskId: z.string().describe('ID de la tarea'),
768
+ integrationId: z.string().optional().describe('Integración (repo) concreta; opcional si solo hay una'),
769
+ branchName: z.string().optional().describe('Nombre de rama explícito; por default "<shortCode>-<slug del título>"'),
770
+ },
771
+ },
772
+ async ({ projectId, taskId, integrationId, branchName }) => {
773
+ try {
774
+ return textResult(await apiPost(config, `/v1/tasks/${taskId}/github-branch`, { projectId: resolveProjectId(projectId), ...(integrationId ? { integrationId } : {}), ...(branchName ? { branchName } : {}) }))
775
+ } catch (err) {
776
+ return errorResult(err)
777
+ }
778
+ }
779
+ )
780
+
781
+ server.registerTool(
782
+ 'open_task_pr',
783
+ {
784
+ description: 'Abre un PR borrador en GitHub desde task.branch hacia la rama base de la integración, con el shortCode y el link a la tarea en el título/cuerpo, y guarda prNumber/prUrl en la tarea. Requiere que la tarea ya tenga branch (create_task_branch primero, o uno puesto a mano). Idempotente: si ya hay un PR abierto para esa rama, devuelve ESE en vez de fallar.',
785
+ inputSchema: {
786
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
787
+ taskId: z.string().describe('ID de la tarea'),
788
+ integrationId: z.string().optional().describe('Integración (repo) concreta; opcional si solo hay una'),
789
+ },
790
+ },
791
+ async ({ projectId, taskId, integrationId }) => {
792
+ try {
793
+ return textResult(await apiPost(config, `/v1/tasks/${taskId}/github-pr`, { projectId: resolveProjectId(projectId), ...(integrationId ? { integrationId } : {}) }))
794
+ } catch (err) {
795
+ return errorResult(err)
796
+ }
797
+ }
798
+ )
799
+
800
+ // Sync de Issues de GitHub <-> tareas (ORQ-203, 10 oct 2026): opt-in POR
801
+ // INTEGRACIÓN (apagado por default). Prenderlo exige issuesModuleId (a
802
+ // qué módulo van las tareas que crea un issue nuevo) -- sin eso, create_task
803
+ // no puede cumplir con "toda tarea requiere módulo". Una vez prendido:
804
+ // issues.opened crea una tarea tipo bug con el link del issue; cerrar la
805
+ // tarea cierra el issue y cerrar el issue cierra la tarea (sin bucles,
806
+ // ambos lados son idempotentes). mirrorComments (opcional, aparte) espeja
807
+ // comentarios nuevos del issue como comentario de sistema en la tarea --
808
+ // solo en ese sentido, comentar en Orquestra no escribe de vuelta en GitHub.
809
+ server.registerTool(
810
+ 'update_github_issues_sync',
811
+ {
812
+ description: 'Prende/apaga el sync de Issues<->tareas de un repo conectado (opt-in por integración). issuesSync:true exige issuesModuleId (módulo donde caen las tareas que crea un issue nuevo). mirrorComments es opcional y aparte: espeja comentarios nuevos del issue como comentario de sistema en la tarea (solo en ese sentido).',
813
+ inputSchema: {
814
+ integrationId: z.string().describe('Integración (repo conectado) a configurar -- list_github_integrations'),
815
+ issuesSync: z.boolean().optional().describe('Prender/apagar la creación automática de tareas desde issues y el cierre cruzado'),
816
+ issuesModuleId: z.string().optional().describe('Módulo donde crear las tareas que originan los issues -- obligatorio para prender issuesSync'),
817
+ mirrorComments: z.boolean().optional().describe('Espejar comentarios nuevos del issue como comentario de sistema en la tarea (GitHub -> Orquestra, no al revés)'),
818
+ },
819
+ },
820
+ async ({ integrationId, issuesSync, issuesModuleId, mirrorComments }) => {
821
+ try {
822
+ return textResult(await apiPatchPath(config, `/v1/github-integrations/${integrationId}/github-issues`, {
823
+ ...(issuesSync !== undefined ? { issuesSync } : {}),
824
+ ...(issuesModuleId !== undefined ? { issuesModuleId } : {}),
825
+ ...(mirrorComments !== undefined ? { mirrorComments } : {}),
826
+ }))
827
+ } catch (err) {
828
+ return errorResult(err)
829
+ }
830
+ }
831
+ )
832
+
753
833
  server.registerTool(
754
834
  'run_closure_scan',
755
835
  {
@@ -924,7 +1004,7 @@ export function createServer() {
924
1004
  server.registerTool(
925
1005
  'publish_content_postproxy',
926
1006
  {
927
- description: 'Publica o programa una pieza de Contenido en redes sociales vía Postproxy -- solo funciona si la pieza tiene al menos una variante en estatus "approved" y su canal ya está conectado (ver connect_postproxy_channel/list_channels). Respeta approvalRequired -- no la llames para saltarte una aprobación pendiente. Cada variante aprobada va en SU PROPIO post de Postproxy (caption/media/fecha propios); antes de cada uno el server verifica el perfil EN VIVO y rechaza con un 400 claro si la cuenta está vencida, ya no existe, o (Instagram/TikTok) vive en el group de Postproxy de otro proyecto -- es decir, el canal apunta a la cuenta de otro proyecto y hay que reconectarlo. Devuelve {posts:[{channelId, postproxyPostId, status}], failed:[{channelId, channelName, error}]}: con éxito parcial responde 200 igual, así que revisa `failed` y cuéntaselo al usuario -- cada variante queda con su externalGuid o su publishError en la pieza.',
1007
+ description: 'Publica o programa una pieza de Contenido en redes sociales vía Postproxy -- solo funciona si la pieza tiene al menos una variante en estatus "approved" y su canal ya está conectado (ver connect_postproxy_channel/list_channels). Si el workspace todavía no tiene Postproxy habilitado (BAJO SOLICITUD, ver connect_postproxy_channel), responde 403 `postproxy_not_enabled` -- no reintentes, dile al usuario que un admin lo solicite desde Contenido → Cuentas conectadas → "Solicitar conexión". Respeta approvalRequired -- no la llames para saltarte una aprobación pendiente. Cada variante aprobada va en SU PROPIO post de Postproxy (caption/media/fecha propios); antes de cada uno el server verifica el perfil EN VIVO y rechaza con un 400 claro si la cuenta está vencida, ya no existe, o (Instagram/TikTok) vive en el group de Postproxy de otro proyecto -- es decir, el canal apunta a la cuenta de otro proyecto y hay que reconectarlo. Devuelve {posts:[{channelId, postproxyPostId, status}], failed:[{channelId, channelName, error}]}: con éxito parcial responde 200 igual, así que revisa `failed` y cuéntaselo al usuario -- cada variante queda con su externalGuid o su publishError en la pieza.',
928
1008
  inputSchema: {
929
1009
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
930
1010
  contentId: z.string().describe('Pieza de Contenido a publicar (ver list_content)'),
@@ -941,6 +1021,79 @@ export function createServer() {
941
1021
  }
942
1022
  )
943
1023
 
1024
+ // sync_content_postproxy / reschedule_content_postproxy (10 oct 2026, a
1025
+ // pedido de José: "todo esto que estamos haciendo en web, también se
1026
+ // puede hacer por MCP?") -- paridad con los botones "Consultar ahora" y
1027
+ // "Reagendar en Postproxy" de ContentDetail.jsx, que hasta ahora solo
1028
+ // existían en la web. Mismo criterio que publish_content_postproxy
1029
+ // (server.registerTool directo, no CRUD estándar).
1030
+ server.registerTool(
1031
+ 'sync_content_postproxy',
1032
+ {
1033
+ description: 'Pide el estado REAL de Postproxy para las variantes ya enviadas de una pieza (con externalGuid), en vez de esperar pasivamente el webhook -- usala si una pieza quedó "esperando confirmación" más tiempo del esperado (el webhook de Postproxy se perdió o tardó) o si el usuario pregunta "¿ya se publicó?" y querés confirmarlo ahora en vez de asumir. Actualiza status/publishedAt/postproxyPermalink/publishError en Firestore si Postproxy tiene novedades, y siempre estampa lastPostproxyCheckAt aunque no haya cambios -- también si la pieza ya está agendada a futuro (scheduledAt), en cuyo caso Postproxy SIEMPRE va a responder "todavía no", no es un error ni señal de que algo falló. Devuelve {checked}: cuántas variantes con externalGuid se consultaron (0 si la pieza nunca se envió a Postproxy). Después de llamarla, volvé a leer la pieza con list_content para ver si cambió -- la respuesta de esta tool no trae el contenido actualizado.',
1034
+ inputSchema: {
1035
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
1036
+ contentId: z.string().describe('Pieza de Contenido a consultar (ver list_content)'),
1037
+ },
1038
+ },
1039
+ async ({ projectId, contentId }) => {
1040
+ try {
1041
+ const data = await apiPost(config, `/v1/content/${contentId}/sync-postproxy`, { projectId: resolveProjectId(projectId) })
1042
+ return textResult(data)
1043
+ } catch (err) {
1044
+ return errorResult(err)
1045
+ }
1046
+ }
1047
+ )
1048
+
1049
+ server.registerTool(
1050
+ 'reschedule_content_postproxy',
1051
+ {
1052
+ description: 'Reagenda en Postproxy un canal de una pieza YA enviado (con externalGuid). Cambiar scheduledAt con update_content SOLO escribe Firestore -- el post real en Postproxy sigue en la hora VIEJA hasta llamar esta tool, así que después de mover la fecha con update_content llamá siempre a esta para que el cambio aplique de verdad. Postproxy solo deja editar drafts o posts programados a MÁS de 5 minutos de su hora real de publicación -- devuelve 422 "Post cannot be edited..." si ya se publicó o está a menos de 5 minutos, ese mensaje sube tal cual, no hay forma de evitarlo. Si sale bien, también actualiza scheduledAt en Firestore para esa variante (no hace falta un update_content aparte). channelId tiene que ser un canal que la pieza YA tenga y que ya esté enviado a Postproxy (ver list_content) -- si no tiene externalGuid todavía, falla con 400 explicando que primero hay que publish_content_postproxy.',
1053
+ inputSchema: {
1054
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
1055
+ contentId: z.string().describe('Pieza de Contenido a reagendar (ver list_content)'),
1056
+ channelId: z.string().describe('Canal ya enviado a Postproxy (con externalGuid) dentro de esa pieza'),
1057
+ scheduledAt: z.number().describe('Nuevo timestamp en ms de cuándo debe publicarse'),
1058
+ },
1059
+ },
1060
+ async ({ projectId, contentId, channelId, scheduledAt }) => {
1061
+ try {
1062
+ const data = await apiPost(config, `/v1/content/${contentId}/reschedule-postproxy`, { projectId: resolveProjectId(projectId), channelId, scheduledAt })
1063
+ return textResult(data)
1064
+ } catch (err) {
1065
+ return errorResult(err)
1066
+ }
1067
+ }
1068
+ )
1069
+
1070
+ // backfill_channel_content (10 oct 2026, a pedido de José: "agregar canal
1071
+ // y agregar los contenidos que aún no se publican... para que todos
1072
+ // estén parejos") -- recorre TODO el proyecto en un solo llamado, no una
1073
+ // pieza a la vez (equivalente en bulk al botón "Copiar contenido acá" de
1074
+ // ContentDetail.jsx en la web, que solo opera sobre una pieza). No es CRUD
1075
+ // estándar (no crea ni edita UNA entidad con un id propio), así que va
1076
+ // con server.registerTool directo, mismo criterio que publish_content_postproxy.
1077
+ server.registerTool(
1078
+ 'backfill_channel_content',
1079
+ {
1080
+ description: 'Agrega un canal a TODAS las piezas de Contenido del proyecto que todavía no lo tienen, copiando caption/media/prompts/formato de otro canal que sí los tenga -- para que un canal recién conectado (ej. LinkedIn) no arranque en blanco pieza por pieza. Solo toca piezas pendientes: se SALTEA automáticamente una pieza si el canal fuente ya está en estatus "published" ahí, si el canal fuente no tiene contenido real que copiar, o si la pieza ya tiene el canal destino. NUNCA pisa fecha (scheduledAt)/aprobación/estatus/desempeño -- el canal nuevo arranca siempre en estatus "idea", sin fecha, cada canal programa/aprueba por separado. Devuelve {updated, updatedIds, skipped, skippedDetail:[{id,title,reason}], failed}: revisa `failed` (reintentos que fallaron la escritura) y contale al usuario cuántas piezas se saltearon y por qué, no asumas que "updated" es el total de piezas del proyecto.',
1081
+ inputSchema: {
1082
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
1083
+ channelId: z.string().describe('Canal a agregar donde falte (ver list_channels) -- recién conectado o con pocas piezas todavía'),
1084
+ sourceChannelId: z.string().describe('Canal del que copiar el contenido en cada pieza (ver list_channels) -- tiene que ser distinto de channelId'),
1085
+ },
1086
+ },
1087
+ async ({ projectId, channelId, sourceChannelId }) => {
1088
+ try {
1089
+ const data = await apiPost(config, '/v1/content/backfill-channel', { projectId: resolveProjectId(projectId), channelId, sourceChannelId })
1090
+ return textResult(data)
1091
+ } catch (err) {
1092
+ return errorResult(err)
1093
+ }
1094
+ }
1095
+ )
1096
+
944
1097
  // Checklist de una tarea -- registradas directo con server.registerTool
945
1098
  // (no con registerCreate/registerUpdate) porque su ruta necesita DOS ids
946
1099
  // dinámicos (taskId + itemId del checklist), y esos helpers solo arman
@@ -1043,11 +1196,11 @@ export function createServer() {
1043
1196
  server.registerTool(
1044
1197
  'list_attachments',
1045
1198
  {
1046
- description: 'Lista los adjuntos (imágenes + código) de una task/note/idea/doc de Orquestra.',
1199
+ description: 'Lista los adjuntos (imágenes + video + código) de una task/note/idea/doc/content de Orquestra.',
1047
1200
  inputSchema: {
1048
1201
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1049
1202
  entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad dueña de los adjuntos'),
1050
- entityId: z.string().describe('ID de la entidad (task/note/idea/doc)'),
1203
+ entityId: z.string().describe('ID de la entidad (task/note/idea/doc/content)'),
1051
1204
  },
1052
1205
  },
1053
1206
  async ({ projectId, entityType, entityId }) => {
@@ -1071,12 +1224,12 @@ export function createServer() {
1071
1224
  server.registerTool(
1072
1225
  'add_attachment',
1073
1226
  {
1074
- description: 'Adjunta evidencia (una imagen o un pedazo de código) a una task/note/idea/doc de Orquestra. Manda EXACTAMENTE uno de filePath (ruta local -- una imagen png/jpg/jpeg/gif/webp se sube a S3, cualquier otra extensión se lee como texto/código) o content (texto/código directo, sin tocar el filesystem). Nunca mandes bytes de imagen en base64 dentro de content -- usa filePath para que este server los suba él mismo.',
1227
+ description: 'Adjunta evidencia (una imagen, un video o un pedazo de código) a una task/note/idea/doc/content de Orquestra. Manda EXACTAMENTE uno de filePath (ruta local -- una imagen png/jpg/jpeg/gif/webp o un video mp4/mov/webm se sube a S3, cualquier otra extensión se lee como texto/código) o content (texto/código directo, sin tocar el filesystem). Nunca mandes bytes de imagen/video en base64 dentro de content -- usa filePath para que este server los suba él mismo.',
1075
1228
  inputSchema: {
1076
1229
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1077
1230
  entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad a la que se adjunta'),
1078
- entityId: z.string().describe('ID de la entidad (task/note/idea/doc)'),
1079
- filePath: z.string().optional().describe('Ruta local a un archivo -- imagen (png/jpg/jpeg/gif/webp) o código/texto.'),
1231
+ entityId: z.string().describe('ID de la entidad (task/note/idea/doc/content)'),
1232
+ filePath: z.string().optional().describe('Ruta local a un archivo -- imagen (png/jpg/jpeg/gif/webp), video (mp4/mov/webm) o código/texto.'),
1080
1233
  content: z.string().optional().describe('Texto/código a adjuntar directo, sin leer un archivo.'),
1081
1234
  language: z.string().optional().describe('Label libre del lenguaje del snippet (ej. "python", "diff") -- solo informativo, no tokeniza (Orquestra no resalta sintaxis).'),
1082
1235
  fileName: z.string().optional().describe('Nombre a mostrar -- si no se manda, sale de filePath o de un default genérico.'),
@@ -1099,11 +1252,15 @@ export function createServer() {
1099
1252
  } else {
1100
1253
  const stat = statSync(filePath)
1101
1254
  const ext = path.extname(filePath).slice(1).toLowerCase()
1102
- const mime = ATTACHMENT_IMAGE_MIME_BY_EXT[ext]
1255
+ const imageMime = ATTACHMENT_IMAGE_MIME_BY_EXT[ext]
1256
+ const videoMime = ATTACHMENT_VIDEO_MIME_BY_EXT[ext]
1257
+ const mime = imageMime || videoMime
1258
+ const kind = imageMime ? 'image' : videoMime ? 'video' : null
1103
1259
  const resolvedFileName = fileName || path.basename(filePath)
1104
1260
  if (mime) {
1105
- if (stat.size > MAX_ATTACHMENT_IMAGE_BYTES) {
1106
- throw new Error(`La imagen supera el máximo de ${MAX_ATTACHMENT_IMAGE_BYTES / 1024 / 1024}MB`)
1261
+ const maxBytes = kind === 'video' ? MAX_ATTACHMENT_VIDEO_BYTES : MAX_ATTACHMENT_IMAGE_BYTES
1262
+ if (stat.size > maxBytes) {
1263
+ throw new Error(`El archivo supera el máximo de ${maxBytes / 1024 / 1024}MB`)
1107
1264
  }
1108
1265
  const buffer = readFileSync(filePath)
1109
1266
  const { uploadUrl, key } = await apiPost(config, '/v1/attachments/upload-url', {
@@ -1111,7 +1268,7 @@ export function createServer() {
1111
1268
  fileName: resolvedFileName, fileType: mime, fileSize: stat.size,
1112
1269
  })
1113
1270
  await putToPresignedUrl(uploadUrl, buffer, mime)
1114
- body = { kind: 'image', key, fileName: resolvedFileName, fileType: mime, fileSize: stat.size, caption }
1271
+ body = { kind, key, fileName: resolvedFileName, fileType: mime, fileSize: stat.size, caption }
1115
1272
  } else {
1116
1273
  const text = readFileSync(filePath, 'utf8')
1117
1274
  if (text.length > MAX_ATTACHMENT_CODE_CHARS) {
@@ -1138,11 +1295,11 @@ export function createServer() {
1138
1295
  server.registerTool(
1139
1296
  'delete_attachment',
1140
1297
  {
1141
- description: 'Borra un adjunto (imagen o código) de una task/note/idea/doc de Orquestra -- limpia también el objeto en S3 si era una imagen. Segunda excepción a "nunca borrado" (junto a delete_checklist_item) -- usalo para limpiar evidencia subida por error, no como reemplazo de mantener historial.',
1298
+ description: 'Borra un adjunto (imagen, video o código) de una task/note/idea/doc/content de Orquestra -- limpia también el objeto en S3 si era imagen o video. Segunda excepción a "nunca borrado" (junto a delete_checklist_item) -- usalo para limpiar evidencia subida por error, no como reemplazo de mantener historial.',
1142
1299
  inputSchema: {
1143
1300
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1144
1301
  entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad dueña del adjunto'),
1145
- entityId: z.string().describe('ID de la entidad (task/note/idea/doc)'),
1302
+ entityId: z.string().describe('ID de la entidad (task/note/idea/doc/content)'),
1146
1303
  attachmentId: z.string().describe('ID del adjunto a borrar (ver list_attachments)'),
1147
1304
  },
1148
1305
  },