taiga-mcp
Version:
MCP server for interacting with Taiga using natural language
406 lines (347 loc) • 19.4 kB
JavaScript
// taigaMCP/openaiClient.js
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import dotenv from 'dotenv';
import axios from 'axios';
import readline from 'readline';
// Cargar variables de entorno
dotenv.config();
// Crear interfaz de línea de comandos
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Función para interactuar con la API de OpenAI
async function queryOpenAI(prompt, purpose = 'tool-selection') {
try {
let systemPrompt = '';
if (purpose === 'tool-selection') {
systemPrompt = 'Eres un asistente que ayuda a interactuar con Taiga a través de un servidor MCP. Traduce las solicitudes del usuario a llamadas a herramientas específicas. Las herramientas disponibles son: listProjects, getProject, createUserStory, y listUserStories. NUNCA sugieras authenticate, ya que la autenticación se maneja automáticamente. Si el usuario menciona "como usuario quiero" o algo similar, responde con "createUserStory". Responde SOLO con el nombre de la herramienta que debe usarse, sin explicaciones adicionales ni código.';
} else if (purpose === 'user-story-extraction') {
systemPrompt = 'Eres un experto en metodologías ágiles y Taiga. Analiza el texto del usuario y extrae una historia de usuario bien formada. Responde en formato JSON con los campos "title" (título corto y descriptivo), "description" (descripción completa en formato "Como [rol], quiero [funcionalidad] para [beneficio]") y "project" (nombre del proyecto mencionado o "default" si no se menciona). Presta especial atención a cualquier mención de proyectos como "TR3_Comunitat", "2DAW_PROJECTEFINAL", etc. Asegúrate de que el título sea conciso y la descripción sea completa.';
} else if (purpose === 'project-selection') {
systemPrompt = 'Eres un asistente que ayuda a seleccionar el proyecto más adecuado para una historia de usuario. Dada una lista de proyectos y una descripción de historia de usuario, selecciona el proyecto más relevante. Responde solo con el slug del proyecto seleccionado.';
} else if (purpose === 'task-creation') {
systemPrompt = 'Eres un experto en desarrollo de software y metodologías ágiles. Dada una historia de usuario, genera una lista de tareas específicas necesarias para implementarla, separadas en tareas de FRONTEND y BACKEND. Responde en formato JSON con un array "tasks" donde cada tarea tiene "title" (título descriptivo), "description" (descripción detallada de la tarea), y "type" ("FRONT" o "BACK"). Incluye todas las tareas necesarias para una implementación completa, considerando aspectos como diseño, validación, seguridad, pruebas, etc.';
}
const response = await axios.post(
'https://api.openai.com/v1/chat/completions',
{
model: 'gpt-4', // Puedes cambiar a otro modelo si lo prefieres
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
temperature: 0.3, // Temperatura más baja para respuestas más consistentes
},
{
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
return response.data.choices[0].message.content.trim();
} catch (error) {
console.error('Error al consultar OpenAI:', error.message);
if (error.response) {
console.error('Detalles del error:', error.response.data);
}
return 'No pude procesar tu solicitud. Por favor, intenta de nuevo.';
}
}
// Función para analizar la respuesta de OpenAI y determinar qué herramienta usar
async function parseOpenAIResponse(response, client, originalPrompt) {
console.log('\nAnálisis de OpenAI:');
console.log(response);
// Simplificar la respuesta para extraer solo el nombre de la herramienta
const toolName = response.toLowerCase().includes('authenticate') ? 'authenticate' :
response.toLowerCase().includes('listprojects') ? 'listProjects' :
response.toLowerCase().includes('getproject') ? 'getProject' :
response.toLowerCase().includes('createuserstory') ? 'createUserStory' :
response.toLowerCase().includes('listuserstories') ? 'listUserStories' :
null;
if (!toolName) {
return { content: [{ type: 'text', text: 'No pude determinar qué acción realizar. Por favor, sé más específico.' }] };
}
console.log(`\nEjecutando herramienta: ${toolName}`);
switch (toolName) {
case 'authenticate':
// Usar las credenciales del archivo .env
console.log('\nRealizando autenticación con credenciales del archivo .env...');
return await client.callTool({
name: 'authenticate',
arguments: {
username: process.env.TAIGA_USERNAME,
password: process.env.TAIGA_PASSWORD
}
});
case 'listProjects':
console.log('\nListando proyectos...');
return await client.callTool({
name: 'listProjects',
arguments: {}
});
case 'getProject':
const projectId = await askQuestion('ID o slug del proyecto: ');
console.log(`\nObteniendo detalles del proyecto ${projectId}...`);
return await client.callTool({
name: 'getProject',
arguments: { projectIdentifier: projectId }
});
case 'createUserStory':
// Verificar si el prompt parece una historia de usuario
if (originalPrompt.toLowerCase().includes('como usuario') ||
originalPrompt.toLowerCase().includes('como administrador') ||
originalPrompt.toLowerCase().includes('como cliente') ||
originalPrompt.toLowerCase().includes('quiero poder')) {
console.log('\nDetectada posible historia de usuario. Extrayendo detalles...');
// Extraer detalles de la historia de usuario usando OpenAI
const userStoryDetails = await queryOpenAI(originalPrompt, 'user-story-extraction');
let userStoryData;
try {
userStoryData = JSON.parse(userStoryDetails);
console.log('\nDetalles extraídos:');
console.log(`Título: ${userStoryData.title}`);
console.log(`Descripción: ${userStoryData.description}`);
console.log(`Proyecto mencionado: ${userStoryData.project}`);
// Obtener la lista de proyectos para seleccionar el más adecuado
const projectsResult = await client.callTool({
name: 'listProjects',
arguments: {}
});
let projectsList = '';
let projects = [];
if (projectsResult && projectsResult.content) {
for (const item of projectsResult.content) {
if (item.type === 'text') {
projectsList = item.text;
// Extraer los proyectos de la respuesta
const lines = projectsList.split('\n').filter(line => line.trim().startsWith('-'));
projects = lines.map(line => {
const match = line.match(/- (.+?) \(ID: (\d+), Slug: (.+?)\)/);
if (match) {
return {
name: match[1],
id: match[2],
slug: match[3]
};
}
return null;
}).filter(p => p !== null);
}
}
}
// Seleccionar el proyecto más adecuado
let selectedProjectSlug;
if (userStoryData.project && userStoryData.project !== 'default') {
// Buscar un proyecto que coincida con el nombre mencionado
const matchingProject = projects.find(p =>
p.name.toLowerCase().includes(userStoryData.project.toLowerCase()) ||
p.slug.toLowerCase().includes(userStoryData.project.toLowerCase())
);
if (matchingProject) {
selectedProjectSlug = matchingProject.slug;
}
}
// Si no se encontró un proyecto, preguntar a OpenAI
if (!selectedProjectSlug && projects.length > 0) {
const projectsInfo = projects.map(p => `${p.name} (${p.slug})`).join('\n');
const projectSelectionPrompt = `Historia de usuario: ${userStoryData.description}\n\nProyectos disponibles:\n${projectsInfo}\n\nSelecciona el proyecto más adecuado para esta historia de usuario.`;
const selectedProject = await queryOpenAI(projectSelectionPrompt, 'project-selection');
// Verificar si la respuesta es un slug válido
const matchingProject = projects.find(p =>
p.slug.toLowerCase() === selectedProject.toLowerCase() ||
p.slug.toLowerCase().includes(selectedProject.toLowerCase()) ||
selectedProject.toLowerCase().includes(p.slug.toLowerCase())
);
if (matchingProject) {
selectedProjectSlug = matchingProject.slug;
}
}
// Si aún no tenemos un proyecto, usar el primero de la lista o preguntar al usuario
if (!selectedProjectSlug) {
if (projects.length > 0) {
selectedProjectSlug = projects[0].slug;
} else {
selectedProjectSlug = await askQuestion('ID o slug del proyecto: ');
}
}
console.log(`\nCreando historia de usuario en proyecto: ${selectedProjectSlug}`);
console.log(`Título: ${userStoryData.title}`);
console.log(`Descripción: ${userStoryData.description}`);
// Crear la historia de usuario
const userStoryResult = await client.callTool({
name: 'createUserStory',
arguments: {
projectIdentifier: selectedProjectSlug,
subject: userStoryData.title,
description: userStoryData.description
}
});
// Extraer el ID de referencia de la historia de usuario creada
let userStoryRef = null;
if (userStoryResult && userStoryResult.content) {
for (const item of userStoryResult.content) {
if (item.type === 'text') {
const match = item.text.match(/Reference: #(\d+)/);
if (match) {
userStoryRef = match[1];
}
}
}
}
// Si tenemos la referencia de la historia de usuario, generar tareas
if (userStoryRef) {
console.log(`\nGenerando tareas para la historia de usuario #${userStoryRef}...`);
// Obtener tecnologías del proyecto o usar valores predeterminados
const technologies = 'React, Node.js, Express, MongoDB'; // Valores predeterminados
// Generar tareas usando OpenAI
const taskPrompt = `Historia de usuario: ${userStoryData.description}\n\nTecnologías: ${technologies}\n\nGenera tareas específicas para implementar esta historia de usuario, separadas en tareas de FRONTEND y BACKEND.`;
const tasksResponse = await queryOpenAI(taskPrompt, 'task-creation');
try {
const tasksData = JSON.parse(tasksResponse);
if (tasksData.tasks && Array.isArray(tasksData.tasks)) {
console.log(`\nCreando ${tasksData.tasks.length} tareas para la historia de usuario #${userStoryRef}...`);
// Crear cada tarea
for (const task of tasksData.tasks) {
const taskTitle = `[${task.type}] ${task.title}`;
console.log(`\nCreando tarea: ${taskTitle}`);
try {
await client.callTool({
name: 'createTask',
arguments: {
projectIdentifier: selectedProjectSlug,
userStoryIdentifier: `#${userStoryRef}`,
subject: taskTitle,
description: task.description
}
});
} catch (taskError) {
console.error(`Error al crear tarea ${taskTitle}:`, taskError.message);
}
}
}
} catch (parseError) {
console.error('Error al procesar las tareas:', parseError.message);
}
}
return userStoryResult;
} catch (error) {
console.error('Error al procesar los detalles de la historia de usuario:', error.message);
console.log('Cambiando a modo manual...');
}
}
// Si no se pudo procesar automáticamente, usar el modo manual
const storyProjectId = await askQuestion('ID o slug del proyecto: ');
const subject = await askQuestion('Título de la historia de usuario: ');
const description = await askQuestion('Descripción (opcional, presiona Enter para omitir): ');
console.log('\nCreando historia de usuario...');
return await client.callTool({
name: 'createUserStory',
arguments: {
projectIdentifier: storyProjectId,
subject,
description: description || undefined
}
});
case 'listUserStories':
const usProjectId = await askQuestion('ID o slug del proyecto: ');
console.log(`\nListando historias de usuario del proyecto ${usProjectId}...`);
return await client.callTool({
name: 'listUserStories',
arguments: { projectIdentifier: usProjectId }
});
default:
return { content: [{ type: 'text', text: 'No pude determinar qué acción realizar. Por favor, sé más específico.' }] };
}
}
// Función auxiliar para hacer preguntas en la consola
function askQuestion(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer);
});
});
}
// Función principal
async function main() {
console.log('Cliente de Taiga MCP con OpenAI');
console.log('===============================');
// Verificar la clave API de OpenAI
if (!process.env.OPENAI_API_KEY) {
console.error('Error: No se encontró la clave API de OpenAI. Por favor, añádela a tu archivo .env');
process.exit(1);
}
// Crear transporte para conectar con el servidor MCP
const transport = new StdioClientTransport({
command: 'node',
args: ['src/index.js'],
});
// Crear cliente MCP
const client = new Client(
{
name: 'Taiga MCP OpenAI Client',
version: '1.0.0',
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
try {
// Conectar al servidor
await client.connect(transport);
console.log('Conectado al servidor MCP de Taiga');
// Autenticar automáticamente al inicio
console.log('\nAutenticando con las credenciales del archivo .env...');
const authResult = await client.callTool({
name: 'authenticate',
arguments: {
username: process.env.TAIGA_USERNAME,
password: process.env.TAIGA_PASSWORD
}
});
if (authResult && authResult.content) {
authResult.content.forEach(item => {
if (item.type === 'text') {
console.log(item.text);
}
});
}
// Bucle principal de interacción
while (true) {
const userInput = await askQuestion('\n¿Qué quieres hacer con Taiga? (escribe "salir" para terminar): ');
if (userInput.toLowerCase() === 'salir') {
break;
}
// Consultar a OpenAI para interpretar la entrada del usuario
console.log('\nConsultando a OpenAI...');
const openaiResponse = await queryOpenAI(userInput);
// Analizar la respuesta y ejecutar la herramienta correspondiente
// Pasamos el prompt original para poder extraer detalles de historias de usuario
const result = await parseOpenAIResponse(openaiResponse, client, userInput);
// Mostrar el resultado
console.log('\nResultado:');
if (result && result.content) {
result.content.forEach(item => {
if (item.type === 'text') {
console.log(item.text);
}
});
} else {
console.log(result);
}
}
console.log('\nGracias por usar el cliente de Taiga MCP. ¡Hasta pronto!');
rl.close();
process.exit(0);
} catch (error) {
console.error('Error:', error);
rl.close();
process.exit(1);
}
}
main();