orquestra-mcp 1.27.0 → 1.31.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.
- package/package.json +3 -3
- package/src/server.mjs +140 -17
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orquestra-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Servidor MCP de Orquestra --
|
|
3
|
+
"version": "1.31.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": "^
|
|
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,
|
|
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 = {
|
|
@@ -299,9 +301,9 @@ export function createServer() {
|
|
|
299
301
|
registerRead(
|
|
300
302
|
'get_parity_summary',
|
|
301
303
|
'/v1/parity',
|
|
302
|
-
'Desfases de paridad multiplataforma: tareas y módulos cuyo scope avanza en una distribución pero sigue pendiente en otra HERMANA (mismo productKey -- ver productGroups en la respuesta; si configured=false, el proyecto nunca marcó qué plataformas son el mismo producto y todas se comparan entre sí: sugiérele al usuario configurarlo con update_distribution/productKey). singleDoneItems trae las candidatas a portar con missingDistIds concretos.',
|
|
304
|
+
'Desfases de paridad multiplataforma: tareas y módulos cuyo scope avanza en una distribución pero sigue pendiente en otra HERMANA (mismo productKey -- ver productGroups en la respuesta; si configured=false, el proyecto nunca marcó qué plataformas son el mismo producto y todas se comparan entre sí: sugiérele al usuario configurarlo con update_distribution/productKey). singleDoneItems trae las candidatas a portar con missingDistIds concretos. Solo cuentan tareas de producto: se excluyen bugs, tareas fusionadas, canceladas/archivadas y las marcadas parityExempt; las candidatas a portar son solo feature/improvement/design (nunca módulos). Un gap es una señal, no un veredicto: el done por plataforma es un flag manual y el trabajo móvil suele existir ya bajo otra tarea -- verifica en el código real de esa plataforma antes de decir que falta algo o de crear una tarea. La respuesta viene compacta: el detalle de gapItems/singleDoneItems está en combined; tasks y modules solo traen totales.',
|
|
303
305
|
{},
|
|
304
|
-
(args, projectId) => ({ projectId })
|
|
306
|
+
(args, projectId) => ({ projectId, compact: '1' })
|
|
305
307
|
)
|
|
306
308
|
|
|
307
309
|
registerRead(
|
|
@@ -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. OJO: en una pieza de Contenido esto NO la deja como media (no se ve ni se publica) -- para eso usa add_content_media. 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
|
|
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
|
-
|
|
1179
|
-
|
|
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
|
|
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) {
|
|
@@ -1202,6 +1286,45 @@ export function createServer() {
|
|
|
1202
1286
|
}
|
|
1203
1287
|
)
|
|
1204
1288
|
|
|
1289
|
+
// Media de una pieza de Contenido (ORQ-283) -- distinto de add_attachment:
|
|
1290
|
+
// un adjunto es evidencia privada, esto es el mediaFiles que Contenido
|
|
1291
|
+
// muestra y Postproxy publica. Mismo criterio de bytes: este server lee el
|
|
1292
|
+
// archivo y lo sube él mismo, nunca base64 por el input.
|
|
1293
|
+
server.registerTool(
|
|
1294
|
+
'add_content_media',
|
|
1295
|
+
{
|
|
1296
|
+
description: 'Pone una imagen o video como MEDIA de una pieza de Contenido (lo que se ve en la pieza y lo que se publica en redes) -- usa esto y NO add_attachment para contenido; add_attachment solo guarda evidencia privada que Contenido no muestra. Sube el archivo local (png/jpg/jpeg/gif/webp/mp4/mov/webm) y lo agrega al mediaFiles sin pisar el que ya haya. Sin channelId lo agrega a todos los canales de la pieza; con channelId solo a esa variante.',
|
|
1297
|
+
inputSchema: {
|
|
1298
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1299
|
+
contentId: z.string().describe('ID de la pieza (list_content)'),
|
|
1300
|
+
filePath: z.string().describe('Ruta local a la imagen o video'),
|
|
1301
|
+
channelId: z.string().optional().describe('Solo si quieres el archivo en un canal específico de una pieza multi-canal'),
|
|
1302
|
+
fileName: z.string().optional().describe('Nombre a mostrar -- default: el del archivo'),
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
async ({ projectId, contentId, filePath, channelId, fileName }) => {
|
|
1306
|
+
try {
|
|
1307
|
+
const resolvedProjectId = resolveProjectId(projectId)
|
|
1308
|
+
const stat = statSync(filePath)
|
|
1309
|
+
const ext = path.extname(filePath).slice(1).toLowerCase()
|
|
1310
|
+
const mime = ATTACHMENT_IMAGE_MIME_BY_EXT[ext] || ATTACHMENT_VIDEO_MIME_BY_EXT[ext]
|
|
1311
|
+
if (!mime) throw new Error('Solo imágenes (png/jpg/jpeg/gif/webp) o videos (mp4/mov/webm)')
|
|
1312
|
+
if (stat.size > MAX_ATTACHMENT_VIDEO_BYTES) throw new Error(`El archivo supera el máximo de ${MAX_ATTACHMENT_VIDEO_BYTES / 1024 / 1024}MB`)
|
|
1313
|
+
const name = fileName || path.basename(filePath)
|
|
1314
|
+
const { uploadUrl, key } = await apiPost(config, '/v1/content/media-upload-url', {
|
|
1315
|
+
projectId: resolvedProjectId, fileName: name, fileType: mime, fileSize: stat.size,
|
|
1316
|
+
})
|
|
1317
|
+
await putToPresignedUrl(uploadUrl, readFileSync(filePath), mime)
|
|
1318
|
+
const data = await apiPost(config, `/v1/content/${contentId}/media`, {
|
|
1319
|
+
projectId: resolvedProjectId, key, name, type: mime, size: stat.size, ...(channelId ? { channelId } : {}),
|
|
1320
|
+
})
|
|
1321
|
+
return textResult(data)
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
return errorResult(err)
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1205
1328
|
// Segunda excepción real a "creación + edición, nunca borrado" (junto a
|
|
1206
1329
|
// delete_checklist_item, ver CLAUDE.md) -- decisión consciente de José:
|
|
1207
1330
|
// un agente puede subir evidencia equivocada (screenshot que no era,
|
|
@@ -1211,11 +1334,11 @@ export function createServer() {
|
|
|
1211
1334
|
server.registerTool(
|
|
1212
1335
|
'delete_attachment',
|
|
1213
1336
|
{
|
|
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
|
|
1337
|
+
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
1338
|
inputSchema: {
|
|
1216
1339
|
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
1217
1340
|
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)'),
|
|
1341
|
+
entityId: z.string().describe('ID de la entidad (task/note/idea/doc/content)'),
|
|
1219
1342
|
attachmentId: z.string().describe('ID del adjunto a borrar (ver list_attachments)'),
|
|
1220
1343
|
},
|
|
1221
1344
|
},
|