UNPKG

@xiaotaitech/tempo-mcp-server

Version:

MCP server for managing Tempo worklogs in Jira

291 lines (290 loc) 11.6 kB
import axios from 'axios'; import config from './config.js'; import { getCurrentUserAccountId, getIssueId } from './jira.js'; import { formatError, getIssueKeysMap, calculateEndTime } from './utils.js'; // API client for Tempo const api = axios.create({ baseURL: config.tempoApi.baseUrl, headers: { 'Authorization': `Bearer ${config.tempoApi.token}`, 'Content-Type': 'application/json', }, }); /** * Retrieve worklogs for the configured user within a date range */ export async function retrieveWorklogs(startDate, endDate) { try { const accountId = await getCurrentUserAccountId(); const response = await api.get(`/worklogs/user/${accountId}`, { params: { from: startDate, to: endDate } }); const worklogs = response.data.results || []; // If no worklogs found, return empty content if (worklogs.length === 0) { return { content: [{ type: "text", text: "No worklogs found for the specified date range." }], }; } // Get issue keys for all worklogs const issueIdToKeyMap = await getIssueKeysMap(worklogs); // Format the response const formattedContent = worklogs.map((worklog) => { const issueId = worklog.issue?.id || 'Unknown'; const issueKey = issueIdToKeyMap[issueId] || 'Unknown'; const description = worklog.description || 'No description'; const timeSpentHours = (worklog.timeSpentSeconds / 3600).toFixed(2); const date = worklog.startDate || 'Unknown'; // Extract Activity attribute as a separate column let activity = ''; if (worklog.attributes && Array.isArray(worklog.attributes.values)) { const activityAttr = worklog.attributes.values.find((attr) => attr.key.replace(/^_+|_+$/g, '') === 'Activity'); activity = activityAttr ? activityAttr.value : ''; } return { type: "text", // Add Activity as a separate column in the output, and include WorklogId for deletion purposes text: `IssueKey: ${issueKey} | IssueId: ${issueId} | WorklogId: ${worklog.tempoWorklogId || worklog.id} | Date: ${date} | Hours: ${timeSpentHours} | Activity: ${activity} | Description: ${description}` }; }); return { content: formattedContent, metadata: { totalCount: worklogs.length, startDate, endDate } }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Error retrieving worklogs: ${formatError(error)}` }] }; } } /** * Create a new worklog */ export async function createWorklog(issueKey, timeSpentHours, date, description = '', startTime = undefined, attributes // Support custom attributes like _Activity_ ) { try { // Get issue ID and account ID const issueId = await getIssueId(issueKey); const accountId = await getCurrentUserAccountId(); // Prepare payload const payload = { issueId, timeSpentSeconds: Math.round(timeSpentHours * 3600), startDate: date, authorAccountId: accountId, description, ...(startTime && { startTime }), }; // Add attributes if provided (e.g., for Activity) if (attributes && attributes.length > 0) { payload.attributes = attributes; } // Submit the worklog const response = await api.post('/worklogs', payload); // Calculate end time if start time is provided let timeInfo = ''; if (startTime) { const endTime = calculateEndTime(startTime, timeSpentHours); timeInfo = ` starting at ${startTime} and ending at ${endTime}`; } return { content: [{ type: "text", text: `Worklog with ID ${response.data.tempoWorklogId} created successfully for ${issueKey}. Time logged: ${timeSpentHours} hours on ${date}${timeInfo}` }] }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Failed to create worklog: ${formatError(error)}` }] }; } } /** * Create multiple worklogs */ export async function bulkCreateWorklogs(worklogEntries, attributes) { try { // Get user account ID const authorAccountId = await getCurrentUserAccountId(); // Group entries by issue key const entriesByIssueKey = {}; worklogEntries.forEach(entry => { if (!entriesByIssueKey[entry.issueKey]) { entriesByIssueKey[entry.issueKey] = []; } entriesByIssueKey[entry.issueKey].push(entry); }); const results = []; const errors = []; // Process each issue's entries for (const [issueKey, entries] of Object.entries(entriesByIssueKey)) { try { const issueId = await getIssueId(issueKey); // Format entries for API const formattedEntries = entries.map(entry => ({ timeSpentSeconds: Math.round(entry.timeSpentHours * 3600), startDate: entry.date, authorAccountId, description: entry.description || '', ...(entry.startTime && { startTime: entry.startTime }), ...(attributes && attributes.length > 0 ? { attributes } : {}) })); // Submit bulk request const response = await api.post(`/worklogs/issue/${issueId}/bulk`, formattedEntries); const createdWorklogs = response.data || []; // Record results entries.forEach((entry, i) => { const created = createdWorklogs[i] || null; // Calculate end time if startTime is provided let endTime = undefined; if (entry.startTime && created) { endTime = calculateEndTime(entry.startTime, entry.timeSpentHours); } results.push({ issueKey, timeSpentHours: entry.timeSpentHours, date: entry.date, worklogId: created?.tempoWorklogId || null, success: !!created, startTime: entry.startTime, endTime }); }); } catch (error) { const errorMessage = formatError(error); // Record errors entries.forEach(entry => { errors.push({ issueKey, timeSpentHours: entry.timeSpentHours, date: entry.date, error: errorMessage }); }); } } // Create content for response const content = []; const successCount = results.filter(r => r.success).length; // Add success messages if (successCount > 0) { content.push({ type: "text", text: `Successfully created ${successCount} worklogs:` }); results.filter(r => r.success).forEach(result => { let timeInfo = ''; if (result.startTime) { timeInfo = ` starting at ${result.startTime}${result.endTime ? ` and ending at ${result.endTime}` : ''}`; } content.push({ type: "text", text: `- Issue ${result.issueKey}: ${result.timeSpentHours} hours on ${result.date}${timeInfo}` }); }); } // Add error messages if (errors.length > 0) { content.push({ type: "text", text: `Failed to create ${errors.length} worklogs:` }); errors.forEach(error => { content.push({ type: "text", text: `- Issue ${error.issueKey}: ${error.timeSpentHours} hours on ${error.date}. Error: ${error.error}` }); }); } return { content, metadata: { totalSuccess: successCount, totalFailure: errors.length, details: { successes: results.filter(r => r.success), failures: errors } }, isError: errors.length > 0 && successCount === 0 }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Error processing bulk worklogs: ${formatError(error)}` }] }; } } /** * Edit an existing worklog */ export async function editWorklog(worklogId, timeSpentHours, description = null, date = null, startTime = undefined, attributes // Support custom attributes like _Activity_ ) { try { // Get current worklog const response = await api.get(`/worklogs/${worklogId}`); const worklog = response.data; // Prepare update payload const updatePayload = { authorAccountId: worklog.author.accountId, startDate: date || worklog.startDate, timeSpentSeconds: Math.round(timeSpentHours * 3600), billableSeconds: Math.round(timeSpentHours * 3600), ...(description !== null && { description }), ...(startTime && { startTime }), }; // Add attributes if provided (e.g., for Activity) if (attributes && attributes.length > 0) { updatePayload.attributes = attributes; } // Update the worklog await api.put(`/worklogs/${worklogId}`, updatePayload); // Information about the update let updateInfo = `Worklog updated successfully`; // Calculate and show time info if we have a start time if (startTime) { const endTime = calculateEndTime(startTime, timeSpentHours); updateInfo += `. Time logged: ${timeSpentHours} hours starting at ${startTime} and ending at ${endTime}`; } // Format response return { content: [{ type: "text", text: updateInfo }], }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Failed to edit worklog: ${formatError(error)}` }] }; } } /** * Delete a worklog */ export async function deleteWorklog(worklogId) { try { // Get worklog details for the response let worklogDetails = null; try { const response = await api.get(`/worklogs/${worklogId}`); worklogDetails = response.data; } catch (error) { // Continue with deletion even if we can't get details console.error(`Could not fetch worklog details: ${error.message}`); } // Delete the worklog await api.delete(`/worklogs/${worklogId}`); return { content: [{ type: "text", text: "Worklog deleted successfully" }], }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Failed to delete worklog: ${formatError(error)}` }] }; } }