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).
257 lines (221 loc) • 8.38 kB
JavaScript
/**
* 評論系統MCP工具
* Comment System MCP Tools for Taiga
*/
import { z } from 'zod';
import { TaigaService } from '../taigaService.js';
import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants.js';
import {
resolveProjectId,
createErrorResponse,
createSuccessResponse,
formatDateTime,
getSafeValue
} from '../utils.js';
const taigaService = new TaigaService();
/**
* 解析並獲取實際的item
* @param {string} itemType - 項目類型
* @param {number} itemId - 項目ID
* @param {string} projectIdentifier - 項目識別符
* @returns {Promise<Object>} - 實際的item
*/
async function resolveAndGetItem(itemType, itemId, projectIdentifier) {
// 對於issues,projectIdentifier是必需的
if (itemType === 'issue' && !projectIdentifier) {
throw new Error('Project identifier is required when working with issues. Please provide projectIdentifier parameter.');
}
// 解析項目ID
const projectId = projectIdentifier ? await resolveProjectId(projectIdentifier) : null;
// 根據itemType獲取實際的item,確保它存在於指定項目中
let actualItem;
if (itemType === 'issue') {
// 對於issue,先嘗試作為ref number,再嘗試作為直接ID
try {
// 首先嘗試作為reference number (如 #829)
actualItem = await taigaService.getIssueByRef(itemId, projectId);
} catch (refError) {
try {
// 如果ref失敗,嘗試作為直接ID
actualItem = await taigaService.getIssue(itemId);
// 檢查是否屬於正確的項目
if (actualItem.project !== projectId) {
throw new Error(`Issue #${itemId} does not belong to project ${projectIdentifier}`);
}
} catch (idError) {
throw new Error(`Issue #${itemId} not found in project ${projectIdentifier}. Tried both ref and ID: ${refError.message}, ${idError.message}`);
}
}
} else {
// 對於user_story和task,直接使用ID
actualItem = { id: itemId };
}
return actualItem;
}
/**
* 添加評論工具
*/
export const addCommentTool = {
name: 'addComment',
schema: {
itemType: z.enum(['issue', 'user_story', 'task']).describe('Type of item to comment on'),
itemId: z.number().describe('ID of the issue, user story, or task'),
projectIdentifier: z.string().optional().describe('Project ID or slug (required for issues)'),
comment: z.string().min(1).describe('Comment content to add')
},
handler: async ({ itemType, itemId, projectIdentifier, comment }) => {
try {
// 檢查認證狀態
if (!taigaService.isAuthenticated()) {
return createErrorResponse(ERROR_MESSAGES.AUTHENTICATION_FAILED);
}
// 解析並獲取實際的item
const actualItem = await resolveAndGetItem(itemType, itemId, projectIdentifier);
// 構建評論數據
const commentData = {
comment: comment
};
// 發送評論到Taiga (通過歷史API)
const response = await taigaService.addComment(itemType, actualItem.id, commentData);
// 格式化響應
const result = formatCommentResponse(response, 'added');
return createSuccessResponse(`${SUCCESS_MESSAGES.COMMENT_ADDED}\n\n${result}`);
} catch (error) {
return createErrorResponse(`${ERROR_MESSAGES.FAILED_TO_ADD_COMMENT}: ${error.message}`);
}
}
};
/**
* 查看評論列表工具
*/
export const listCommentsTool = {
name: 'listComments',
schema: {
itemType: z.enum(['issue', 'user_story', 'task']).describe('Type of item to get comments for'),
itemId: z.number().describe('ID of the issue, user story, or task'),
projectIdentifier: z.string().optional().describe('Project ID or slug (required for issues)')
},
handler: async ({ itemType, itemId, projectIdentifier }) => {
try {
// 檢查認證狀態
if (!taigaService.isAuthenticated()) {
return createErrorResponse(ERROR_MESSAGES.AUTHENTICATION_FAILED);
}
// 解析並獲取實際的item
const actualItem = await resolveAndGetItem(itemType, itemId, projectIdentifier);
// 獲取項目歷史記錄(包含評論)
const history = await taigaService.getItemHistory(itemType, actualItem.id);
// 過濾出評論相關的歷史記錄
const comments = filterCommentsFromHistory(history);
if (!comments || comments.length === 0) {
return createSuccessResponse(`**${itemType} #${itemId} 評論列表**\n\n目前沒有評論`);
}
// 格式化評論列表
const formattedComments = formatCommentsList(comments, itemType, itemId);
return createSuccessResponse(formattedComments);
} catch (error) {
return createErrorResponse(`${ERROR_MESSAGES.FAILED_TO_LIST_COMMENTS}: ${error.message}`);
}
}
};
/**
* 編輯評論工具
*/
export const editCommentTool = {
name: 'editComment',
schema: {
commentId: z.number().describe('ID of the comment to edit'),
newComment: z.string().min(1).describe('New comment content')
},
handler: async ({ commentId, newComment }) => {
try {
// 編輯評論
const response = await taigaService.editComment(commentId, newComment);
// 格式化響應
const result = formatCommentResponse(response, 'edited');
return createSuccessResponse(`${SUCCESS_MESSAGES.COMMENT_EDITED}\n\n${result}`);
} catch (error) {
if (error.response?.status === 404) {
return createErrorResponse(ERROR_MESSAGES.COMMENT_NOT_FOUND);
}
return createErrorResponse(`${ERROR_MESSAGES.FAILED_TO_EDIT_COMMENT}: ${error.message}`);
}
}
};
/**
* 刪除評論工具
*/
export const deleteCommentTool = {
name: 'deleteComment',
schema: {
commentId: z.number().describe('ID of the comment to delete')
},
handler: async ({ commentId }) => {
try {
// 刪除評論
await taigaService.deleteComment(commentId);
return createSuccessResponse(`${SUCCESS_MESSAGES.COMMENT_DELETED}\n\n評論 #${commentId} 已成功刪除`);
} catch (error) {
if (error.response?.status === 404) {
return createErrorResponse(ERROR_MESSAGES.COMMENT_NOT_FOUND);
}
return createErrorResponse(`${ERROR_MESSAGES.FAILED_TO_DELETE_COMMENT}: ${error.message}`);
}
}
};
/**
* 從歷史記錄中過濾出評論
*/
function filterCommentsFromHistory(history) {
if (!Array.isArray(history)) return [];
return history.filter(entry =>
// Taiga API returns type as number 1 for changes, not string 'change'
entry.type === 1 &&
entry.comment &&
entry.comment.trim().length > 0
);
}
/**
* 格式化評論列表
*/
function formatCommentsList(comments, itemType, itemId) {
let output = `**${itemType.replace('_', ' ')} #${itemId} 評論列表**\n\n`;
output += `共 ${comments.length} 個評論\n\n`;
comments.forEach((comment, index) => {
const user = comment.user?.full_name || comment.user?.username || '未知用戶';
const createdDate = formatDateTime(comment.created_at);
const commentText = comment.comment || '無內容';
output += `**${index + 1}. ${user}** ${createdDate}\n`;
output += `${commentText}\n`;
if (comment.id) {
output += `評論ID: ${comment.id}\n`;
}
output += '\n';
});
return output;
}
/**
* 格式化單個評論響應
*/
function formatCommentResponse(response, action) {
const user = response.user?.full_name || response.user?.username || '未知用戶';
const createdDate = formatDateTime(response.created_at);
const commentText = response.comment || '無內容';
let output = `**評論已${action === 'added' ? '添加' : '編輯'}**\n\n`;
output += `用戶: ${user}\n`;
output += `時間: ${createdDate}\n`;
output += `內容: ${commentText}\n`;
if (response.id) {
output += `評論ID: ${response.id}`;
}
return output;
}
/**
* 註冊評論系統工具
*/
export function registerCommentTools(server) {
server.tool(addCommentTool.name, addCommentTool.schema, addCommentTool.handler);
server.tool(listCommentsTool.name, listCommentsTool.schema, listCommentsTool.handler);
server.tool(editCommentTool.name, editCommentTool.schema, editCommentTool.handler);
server.tool(deleteCommentTool.name, deleteCommentTool.schema, deleteCommentTool.handler);
}