Skip to main content
Glama

Mantis MCP Server

by kfnzero
server.ts17 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { isMantisConfigured } from "./config/index.js"; import mantisApi, { MantisApiError, User } from "./services/mantisApi.js"; import { log } from "./utils/logger.js"; import { gzip } from 'zlib'; import { promisify } from 'util'; const gzipAsync = promisify(gzip); // 定義壓縮閾值(單位:字節) const COMPRESSION_THRESHOLD = 1024 * 100; // 100KB // 定義日誌數據類型 interface LogData { tool: string; [key: string]: any; error?: any; } // 高階函數:檢查 Mantis 配置並執行工具邏輯 async function withMantisConfigured<T>( toolName: string, action: () => Promise<T> ): Promise<{ [x: string]: unknown; content: Array<{ [x: string]: unknown; type: "text"; text: string; }>; _meta?: { [key: string]: unknown } | undefined; isError?: boolean | undefined; }> { try { // 檢查是否已配置 Mantis API if (!isMantisConfigured()) { return { content: [ { type: "text", text: JSON.stringify( { error: "Mantis API 尚未配置", message: "請在環境變數中設定 MANTIS_API_URL 和 MANTIS_API_KEY" }, null, 2 ), }, ], isError: true }; } // 執行工具邏輯 const result = await action(); return { content: [ { type: "text", text: typeof result === 'string' ? result : JSON.stringify(result, null, 2), }, ], }; } catch (error) { // 處理錯誤情況 let errorMessage = `執行 ${toolName} 時發生錯誤`; let logData: LogData = { tool: toolName }; if (error instanceof MantisApiError) { errorMessage = `Mantis API 錯誤: ${error.message}`; if (error.statusCode) { errorMessage += ` (HTTP ${error.statusCode})`; logData = { ...logData, statusCode: error.statusCode }; } log.error(errorMessage, { ...logData, error: error.message }); } else if (error instanceof Error) { errorMessage = error.message; log.error(errorMessage, { ...logData, error: error.stack }); } else { log.error(errorMessage, { ...logData, error }); } return { content: [ { type: "text", text: JSON.stringify( { error: errorMessage, }, null, 2 ), }, ], isError: true }; } } // 壓縮 JSON 數據 async function compressJsonData(data: any): Promise<string> { const jsonString = JSON.stringify(data); if (jsonString.length < COMPRESSION_THRESHOLD) { return jsonString; } const compressed = await gzipAsync(Buffer.from(jsonString)); return compressed.toString('base64'); } export function createServer(): McpServer { const server = new McpServer({ name: "Mantis MCP Server", version: "0.1.0", }); // 獲取問題列表 server.tool( "get_issues", "獲取 Mantis 問題列表,可根據多個條件進行過濾,建議查詢時select選擇id,summary,description就好,資訊過多可能導致程式異常", { projectId: z.number().optional().describe("專案 ID"), statusId: z.number().optional().describe("狀態 ID"), handlerId: z.number().optional().describe("處理人 ID"), reporterId: z.number().optional().describe("報告者 ID"), search: z.string().optional().describe("搜尋關鍵字"), pageSize: z.number().optional().default(20).describe("頁數大小"), page: z.number().optional().default(0).describe("分頁起始位置,從1開始"), select: z.array(z.string()).optional().describe("選擇要返回的欄位,例如:['id', 'summary', 'description']"), }, async (params) => { return withMantisConfigured("get_issues", async () => { const issues = await mantisApi.getIssues(params); const jsonString = JSON.stringify(issues); if (jsonString.length < COMPRESSION_THRESHOLD) { return jsonString; } const compressed = await gzipAsync(Buffer.from(jsonString)); const base64Data = compressed.toString('base64'); return JSON.stringify({ compressed: true, data: base64Data, originalSize: jsonString.length, compressedSize: base64Data.length }); }); } ); // 根據 ID 獲取問題詳情 server.tool( "get_issue_by_id", "根據 ID 獲取 Mantis 問題詳情", { issueId: z.number().describe("問題 ID"), }, async ({ issueId }) => { return withMantisConfigured("get_issue_by_id", async () => { const issue = await mantisApi.getIssueById(issueId); return JSON.stringify(issue, null, 2); }); } ); // 根據用戶名稱查詢用戶 server.tool( "get_user", "根據用戶名稱查詢 Mantis 用戶", { username: z.string().describe("用戶名稱") }, async (params) => { return withMantisConfigured("get_user", async () => { const user = await mantisApi.getUserByUsername(params.username); return JSON.stringify(user, null, 2); }); } ); // 獲取專案列表 server.tool( "get_projects", "獲取 Mantis 專案列表", {}, async () => { return withMantisConfigured("get_projects", async () => { const projects = await mantisApi.getProjects(); return JSON.stringify(projects, null, 2); }); } ); // 獲取問題統計 server.tool( "get_issue_statistics", "獲取 Mantis 問題統計數據,根據不同維度進行分析", { projectId: z.number().optional().describe("專案 ID"), groupBy: z.enum(['status', 'priority', 'severity', 'handler', 'reporter']).describe("分組依據"), period: z.enum(['all', 'today', 'week', 'month']).default('all').describe("時間範圍<all-全部, today-今天, week-本週, month-本月>"), }, async (params) => { return withMantisConfigured("get_issue_statistics", async () => { // 從 Mantis API 獲取問題並處理統計 const issues = await mantisApi.getIssues({ projectId: params.projectId, pageSize: 1000 // 獲取大量數據用於統計 }); // 建立統計結果 const statistics = { total: issues.length, groupedBy: params.groupBy, period: params.period, data: {} as Record<string, number> }; // 根據時間範圍過濾 let filteredIssues = issues; log.debug("根據時間範圍過濾issues", { issues, params }); const now = new Date(); const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const startOfWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay()); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); switch (params.period) { case 'today': filteredIssues = issues.filter(issue => { const createdAt = new Date(issue.created_at); return createdAt >= startOfDay; }); break; case 'week': filteredIssues = issues.filter(issue => { const createdAt = new Date(issue.created_at); return createdAt >= startOfWeek; }); break; case 'month': filteredIssues = issues.filter(issue => { const createdAt = new Date(issue.created_at); return createdAt >= startOfMonth; }); break; case 'all': default: // 保持原有的issues不變 break; } if (!filteredIssues || filteredIssues.length === 0) { return { error: "沒有查詢到任何Issue" }; } // 根據分組依據進行統計 filteredIssues.forEach(issue => { let key = ''; switch (params.groupBy) { case 'status': key = issue.status?.name || 'unknown'; break; case 'priority': key = issue.priority?.name || 'unknown'; break; case 'severity': key = issue.severity?.name || 'unknown'; break; case 'handler': key = issue.handler?.name || 'unassigned'; break; case 'reporter': key = issue.reporter?.name || 'unknown'; break; } statistics.data[key] = (statistics.data[key] || 0) + 1; }); return JSON.stringify(statistics, null, 2); }); } ); // 獲取分派統計 server.tool( "get_assignment_statistics", "獲取 Mantis 問題分派統計數據,分析不同用戶的問題分派情況", { projectId: z.number().optional().describe("專案 ID"), includeUnassigned: z.boolean().default(true).describe("是否包含未分派問題"), statusFilter: z.array(z.number()).optional().describe("狀態過濾器,只計算特定狀態的問題"), }, async (params) => { return withMantisConfigured("get_assignment_statistics", async () => { // 獲取問題 const issues = await mantisApi.getIssues({ projectId: params.projectId, pageSize: 1000 // 獲取大量數據用於統計 }); // 過濾問題 let filteredIssues = issues; if (params.statusFilter?.length) { filteredIssues = issues.filter(issue => params.statusFilter?.includes(issue.status.id) ); } // 建立用戶問題統計 const userMap = new Map<number, { id: number; name: string; email: string; issueCount: number; openIssues: number; closedIssues: number; issues: number[]; }>(); // 從問題中收集所有處理人ID const handlerIds = new Set<number>(); filteredIssues.forEach(issue => { if (issue.handler?.id) { handlerIds.add(issue.handler.id); } }); // 查詢每個處理人的詳細資訊並初始化統計 for (const handlerId of handlerIds) { const user = await mantisApi.getUser(handlerId); userMap.set(user.id, { id: user.id, name: user.name, email: user.email || '', issueCount: 0, openIssues: 0, closedIssues: 0, issues: [] }); } // 未分派問題統計 let unassignedCount = 0; let unassignedIssues: number[] = []; // 計算統計 filteredIssues.forEach(issue => { if (issue.handler && issue.handler.id) { const userStat = userMap.get(issue.handler.id); if (userStat) { userStat.issueCount++; userStat.issues.push(issue.id); // 根據狀態判斷是否為關閉狀態 if (issue.status.name.toLowerCase().includes('closed') || issue.status.name.toLowerCase().includes('resolved')) { userStat.closedIssues++; } else { userStat.openIssues++; } } } else if (params.includeUnassigned) { unassignedCount++; unassignedIssues.push(issue.id); } }); // 構建結果 const statistics = { totalIssues: filteredIssues.length, assignedIssues: filteredIssues.length - unassignedCount, unassignedIssues: unassignedCount, userStatistics: Array.from(userMap.values()) .filter(stat => stat.issueCount > 0) .sort((a, b) => b.issueCount - a.issueCount) }; if (params.includeUnassigned && unassignedCount > 0) { statistics.userStatistics.push({ id: 0, name: "未分派", email: "", issueCount: unassignedCount, openIssues: unassignedCount, closedIssues: 0, issues: unassignedIssues }); } return JSON.stringify(statistics, null, 2); }); } ); // 獲取指定專案的所有用戶 server.tool( "get_users_by_project_id", "獲取指定專案的所有用戶", { projectId: z.number().describe("專案 ID"), }, async (params) => { return withMantisConfigured("get_users_by_project_id", async () => { const users = await mantisApi.getUsersByProjectId(params.projectId); return JSON.stringify(users, null, 2); }); } ); // 獲取所有用戶 server.tool( "get_users", "用暴力法強制取得所有用戶", {}, async () => { return withMantisConfigured("get_users", async () => { let notFoundCount = 0; let id = 1; let users: User[] = []; do { try { const user = await mantisApi.getUser(id); users.push(user); id++; notFoundCount = 0; // 重置計數器 } catch (error) { if (error instanceof MantisApiError && error.statusCode === 404) { notFoundCount++; id++; } } } while (notFoundCount < 10); return JSON.stringify(users, null, 2); }); } ); // 新增 issue server.tool( "create_issue", "新增一個 Mantis 問題", { summary: z.string().describe("問題摘要"), description: z.string().describe("問題詳細描述"), projectId: z.number().describe("專案 ID"), categoryId: z.number().optional().describe("分類 ID"), handlerId: z.number().optional().describe("處理人 ID"), priority: z.string().optional().describe("優先級"), severity: z.string().optional().describe("嚴重性"), additional_information: z.string().optional().describe("附加信息"), }, async (params) => { return withMantisConfigured("create_issue", async () => { const issueData = { summary: params.summary, description: params.description, project: { id: params.projectId }, category: { id: params.categoryId || 1 }, // 默認 category handler: params.handlerId ? { id: params.handlerId } : undefined, priority: params.priority ? { name: params.priority } : undefined, severity: params.severity ? { name: params.severity } : undefined, additional_information: params.additional_information, }; const issue = await mantisApi.createIssue(issueData); return JSON.stringify(issue, null, 2); }); } ); // 修改 issue server.tool( "update_issue", "修改一個 Mantis 問題", { issueId: z.number().describe("問題 ID"), summary: z.string().optional().describe("問題摘要"), description: z.string().optional().describe("問題詳細描述"), handlerId: z.number().optional().describe("處理人 ID"), status: z.string().optional().describe("狀態"), resolution: z.string().optional().describe("解決方案"), priority: z.string().optional().describe("優先級"), severity: z.string().optional().describe("嚴重性"), }, async (params) => { return withMantisConfigured("update_issue", async () => { const updateData = { summary: params.summary, description: params.description, handler: params.handlerId ? { id: params.handlerId } : undefined, status: params.status ? { name: params.status } : undefined, resolution: params.resolution ? { name: params.resolution } : undefined, priority: params.priority ? { name: params.priority } : undefined, severity: params.severity ? { name: params.severity } : undefined, }; const issue = await mantisApi.updateIssue(params.issueId, updateData); return JSON.stringify(issue, null, 2); }); } ); // 新增 issue note server.tool( "add_issue_note", "為一個 Mantis 問題新增備註", { issueId: z.number().describe("問題 ID"), text: z.string().describe("備註內容"), view_state: z.string().optional().default("public").describe("可見狀態 (public 或 private)"), }, async (params) => { return withMantisConfigured("add_issue_note", async () => { const noteData = { text: params.text, view_state: { name: params.view_state }, }; const result = await mantisApi.addIssueNote(params.issueId, noteData); return JSON.stringify(result, null, 2); }); } ); return server; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kfnzero/mantis-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server