UNPKG

taiga-mcp-server

Version:

Full-featured MCP server for Taiga project management with advanced sprint and issue tracking. Based on mcpTAIGA by adriapedralbes, enhanced with substantial modifications. Developed collaboratively with Claude Code (AI-assisted development).

240 lines (216 loc) 7.46 kB
/** * Utility functions for Taiga MCP Server */ import { TaigaService } from './taigaService.js'; import { STATUS_LABELS } from './constants.js'; const taigaService = new TaigaService(); /** * Resolve project identifier to project ID * @param {string} projectIdentifier - Project ID or slug * @returns {Promise<string>} - Project ID */ export async function resolveProjectId(projectIdentifier) { if (!isNaN(projectIdentifier)) { return projectIdentifier; } const project = await taigaService.getProjectBySlug(projectIdentifier); return project.id; } /** * Resolve issue identifier to issue object * @param {string} issueIdentifier - Issue ID or reference (#123) * @param {string} [projectIdentifier] - Project ID or slug (required for reference) * @returns {Promise<Object>} - Issue object */ export async function resolveIssue(issueIdentifier, projectIdentifier) { // Handle #-prefixed reference numbers if (issueIdentifier.startsWith('#')) { if (!projectIdentifier) { throw new Error('Project identifier is required when using issue reference number'); } const projectId = await resolveProjectId(projectIdentifier); const ref = issueIdentifier.substring(1); return await taigaService.getIssueByRef(ref, projectId); } // For pure numbers, try both approaches: first as ID, then as reference number if (/^\d+$/.test(issueIdentifier)) { // First try as direct Issue ID try { return await taigaService.getIssue(issueIdentifier); } catch (error) { // If that fails and we have a project identifier, try as reference number if (projectIdentifier) { try { const projectId = await resolveProjectId(projectIdentifier); return await taigaService.getIssueByRef(issueIdentifier, projectId); } catch (refError) { // If both fail, throw a more helpful error throw new Error(`Issue not found by ID "${issueIdentifier}" or reference number "#${issueIdentifier}" in project. Original errors: ID lookup: ${error.message}, Ref lookup: ${refError.message}`); } } else { // No project identifier available, can only try ID throw new Error(`Issue ID "${issueIdentifier}" not found. If this is a reference number, please provide projectIdentifier or use "#${issueIdentifier}" format.`); } } } // For non-numeric strings, treat as direct ID return await taigaService.getIssue(issueIdentifier); } /** * Find status ID by name * @param {Array} statuses - Array of status objects * @param {string} statusName - Status name to find * @returns {number|undefined} - Status ID or undefined if not found */ export function findStatusIdByName(statuses, statusName) { if (!statusName) return undefined; const status = statuses.find(s => s.name.toLowerCase() === statusName.toLowerCase() ); return status?.id; } /** * Find ID by name in a collection * @param {Array} collection - Array of objects with name property * @param {string} name - Name to find * @returns {number|undefined} - ID or undefined if not found */ export function findIdByName(collection, name) { if (!name) return undefined; const item = collection.find(item => item.name.toLowerCase() === name.toLowerCase() ); return item?.id; } /** * Format date for display * @param {string} dateString - ISO date string * @returns {string} - Formatted date or 'Not set' */ export function formatDate(dateString) { if (!dateString) return 'Not set'; return new Date(dateString).toLocaleDateString(); } /** * Format datetime for display * @param {string} dateString - ISO date string * @returns {string} - Formatted datetime */ export function formatDateTime(dateString) { if (!dateString) return 'Not set'; return new Date(dateString).toLocaleString(); } /** * Calculate completion percentage * @param {number} completed - Completed items * @param {number} total - Total items * @returns {number} - Percentage (0-100) */ export function calculateCompletionPercentage(completed, total) { if (!total || total === 0) return 0; return Math.round((completed / total) * 100); } /** * Create MCP error response * @param {string} message - Error message * @returns {Object} - MCP error response */ export function createErrorResponse(message) { return { content: [ { type: 'text', text: `❌ Error: ${message}`, }, ], isError: true, }; } /** * Create MCP success response * @param {string} text - Response text * @returns {Object} - MCP success response */ export function createSuccessResponse(text) { return { content: [ { type: 'text', text, }, ], }; } /** * Get status label (Active/Closed) * @param {boolean} closed - Whether item is closed * @returns {string} - Status label */ export function getStatusLabel(closed) { return closed ? STATUS_LABELS.CLOSED : STATUS_LABELS.ACTIVE; } /** * Get safe value or default * @param {any} value - Value to check * @param {string} defaultValue - Default value if null/undefined * @returns {string} - Safe value */ export function getSafeValue(value, defaultValue = STATUS_LABELS.UNKNOWN) { return value || defaultValue; } /** * Format project list for display * @param {Array} projects - Array of project objects * @returns {string} - Formatted project list */ export function formatProjectList(projects) { return projects.map(p => `- ${p.name} (ID: ${p.id}, Slug: ${p.slug})`).join('\n'); } /** * Format user story list for display * @param {Array} userStories - Array of user story objects * @returns {string} - Formatted user story list */ export function formatUserStoryList(userStories) { return userStories.map(us => `- #${us.ref}: ${us.subject} (Status: ${getSafeValue(us.status_extra_info?.name)})` ).join('\n'); } /** * Format issue list for display * @param {Array} issues - Array of issue objects * @returns {string} - Formatted issue list */ export function formatIssueList(issues) { return issues.map(issue => `- #${issue.ref}: ${issue.subject} Status: ${getSafeValue(issue.status_extra_info?.name)} Priority: ${getSafeValue(issue.priority_extra_info?.name)} Sprint: ${getSafeValue(issue.milestone_extra_info?.name, STATUS_LABELS.NO_SPRINT)} Assigned: ${getSafeValue(issue.assigned_to_extra_info?.full_name, STATUS_LABELS.UNASSIGNED)}`).join('\n\n'); } /** * Format sprint list for display * @param {Array} sprints - Array of sprint objects * @returns {string} - Formatted sprint list */ export function formatSprintList(sprints) { return sprints.map(sprint => { const startDate = formatDate(sprint.estimated_start); const endDate = formatDate(sprint.estimated_finish); const status = getStatusLabel(sprint.closed); return `- ${sprint.name} (ID: ${sprint.id}) Status: ${status} Duration: ${startDate} ~ ${endDate}`; }).join('\n\n'); } /** * Format sprint issues for display * @param {Array} issues - Array of issue objects * @returns {string} - Formatted issue list for sprint */ export function formatSprintIssues(issues) { return issues.map(issue => `🔸 #${issue.ref}: ${issue.subject} Status: ${getSafeValue(issue.status_extra_info?.name)} Priority: ${getSafeValue(issue.priority_extra_info?.name)} Assigned: ${getSafeValue(issue.assigned_to_extra_info?.full_name, STATUS_LABELS.UNASSIGNED)}`).join('\n\n'); }