orquestra-mcp 1.27.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 +99 -15
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.27.0",
4
- "description": "Servidor MCP de Orquestra -- 112 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
  {
@@ -1116,11 +1196,11 @@ export function createServer() {
1116
1196
  server.registerTool(
1117
1197
  'list_attachments',
1118
1198
  {
1119
- 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.',
1120
1200
  inputSchema: {
1121
1201
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1122
1202
  entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad dueña de los adjuntos'),
1123
- 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)'),
1124
1204
  },
1125
1205
  },
1126
1206
  async ({ projectId, entityType, entityId }) => {
@@ -1144,12 +1224,12 @@ export function createServer() {
1144
1224
  server.registerTool(
1145
1225
  'add_attachment',
1146
1226
  {
1147
- 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.',
1148
1228
  inputSchema: {
1149
1229
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1150
1230
  entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad a la que se adjunta'),
1151
- entityId: z.string().describe('ID de la entidad (task/note/idea/doc)'),
1152
- 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.'),
1153
1233
  content: z.string().optional().describe('Texto/código a adjuntar directo, sin leer un archivo.'),
1154
1234
  language: z.string().optional().describe('Label libre del lenguaje del snippet (ej. "python", "diff") -- solo informativo, no tokeniza (Orquestra no resalta sintaxis).'),
1155
1235
  fileName: z.string().optional().describe('Nombre a mostrar -- si no se manda, sale de filePath o de un default genérico.'),
@@ -1172,11 +1252,15 @@ export function createServer() {
1172
1252
  } else {
1173
1253
  const stat = statSync(filePath)
1174
1254
  const ext = path.extname(filePath).slice(1).toLowerCase()
1175
- 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
1176
1259
  const resolvedFileName = fileName || path.basename(filePath)
1177
1260
  if (mime) {
1178
- if (stat.size > MAX_ATTACHMENT_IMAGE_BYTES) {
1179
- 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`)
1180
1264
  }
1181
1265
  const buffer = readFileSync(filePath)
1182
1266
  const { uploadUrl, key } = await apiPost(config, '/v1/attachments/upload-url', {
@@ -1184,7 +1268,7 @@ export function createServer() {
1184
1268
  fileName: resolvedFileName, fileType: mime, fileSize: stat.size,
1185
1269
  })
1186
1270
  await putToPresignedUrl(uploadUrl, buffer, mime)
1187
- body = { kind: 'image', key, fileName: resolvedFileName, fileType: mime, fileSize: stat.size, caption }
1271
+ body = { kind, key, fileName: resolvedFileName, fileType: mime, fileSize: stat.size, caption }
1188
1272
  } else {
1189
1273
  const text = readFileSync(filePath, 'utf8')
1190
1274
  if (text.length > MAX_ATTACHMENT_CODE_CHARS) {
@@ -1211,11 +1295,11 @@ export function createServer() {
1211
1295
  server.registerTool(
1212
1296
  'delete_attachment',
1213
1297
  {
1214
- 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.',
1215
1299
  inputSchema: {
1216
1300
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
1217
1301
  entityType: z.enum(ATTACHMENT_ENTITY_TYPES).describe('Tipo de entidad dueña del adjunto'),
1218
- 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)'),
1219
1303
  attachmentId: z.string().describe('ID del adjunto a borrar (ver list_attachments)'),
1220
1304
  },
1221
1305
  },