UNPKG

redmine-mcp-tools

Version:

A comprehensive Model Context Protocol (MCP) server for Redmine integration. Provides 25+ specialized tools for complete Redmine API access including issue management, project administration, time tracking, and user management. Built with TypeScript and d

307 lines (306 loc) 13.2 kB
import { asNumber, extractPaginationParams, ValidationError, } from "./types.js"; /** * Creates handlers for issue-related operations */ export function createIssuesHandlers(context) { const { client } = context; return { /** * Lists issues with pagination and filters */ list_issues: async (args) => { try { if (typeof args !== 'object' || args === null) { throw new ValidationError("Arguments must be an object"); } const argsObj = args; const { limit, offset } = extractPaginationParams(argsObj); // Build query parameters const queryParams = { limit, offset, }; if ('sort' in argsObj) queryParams.sort = String(argsObj.sort); if ('include' in argsObj) queryParams.include = String(argsObj.include); if ('project_id' in argsObj) queryParams.project_id = String(argsObj.project_id); if ('issue_id' in argsObj) queryParams.issue_id = String(argsObj.issue_id); if ('tracker_id' in argsObj) queryParams.tracker_id = asNumber(argsObj.tracker_id); if ('status_id' in argsObj) queryParams.status_id = String(argsObj.status_id); if ('assigned_to_id' in argsObj) queryParams.assigned_to_id = String(argsObj.assigned_to_id); if ('parent_id' in argsObj) queryParams.parent_id = asNumber(argsObj.parent_id); if ('created_on' in argsObj) queryParams.created_on = String(argsObj.created_on); if ('updated_on' in argsObj) queryParams.updated_on = String(argsObj.updated_on); if ('start_date' in argsObj) queryParams.start_date = String(argsObj.start_date); if ('due_date' in argsObj) queryParams.due_date = String(argsObj.due_date); if ('custom_field_id' in argsObj) queryParams.custom_field_id = String(argsObj.custom_field_id); const queryString = client.encodeQueryParams(queryParams); const url = `issues.json${queryString ? '?' + queryString : ''}`; const response = await client.performRequest(url); return { content: [{ type: "text", text: `Found ${response.total_count || 0} issues (showing ${response.issues?.length || 0}):\n\n` + JSON.stringify(response, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error listing issues: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }, /** * Get a specific issue */ get_issue: async (args) => { try { if (typeof args !== 'object' || args === null) { throw new ValidationError("Arguments must be an object"); } const argsObj = args; const issueId = asNumber(argsObj.issue_id); const queryParams = {}; if ('include' in argsObj) queryParams.include = String(argsObj.include); const queryString = client.encodeQueryParams(queryParams); const url = `issues/${issueId}.json${queryString ? '?' + queryString : ''}`; const response = await client.performRequest(url); return { content: [{ type: "text", text: `Issue #${issueId}:\n\n` + JSON.stringify(response.issue, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error getting issue: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }, /** * Create a new issue */ create_issue: async (args) => { try { if (typeof args !== 'object' || args === null) { throw new ValidationError("Arguments must be an object"); } const argsObj = args; const issueData = { project: { id: asNumber(argsObj.project_id) }, subject: String(argsObj.subject), }; if ('tracker_id' in argsObj) issueData.tracker = { id: asNumber(argsObj.tracker_id) }; if ('status_id' in argsObj) issueData.status = { id: asNumber(argsObj.status_id) }; if ('priority_id' in argsObj) issueData.priority = { id: asNumber(argsObj.priority_id) }; if ('description' in argsObj) issueData.description = String(argsObj.description); if ('assigned_to_id' in argsObj) issueData.assigned_to = { id: asNumber(argsObj.assigned_to_id) }; if ('start_date' in argsObj) issueData.start_date = String(argsObj.start_date); if ('due_date' in argsObj) issueData.due_date = String(argsObj.due_date); if ('estimated_hours' in argsObj) issueData.estimated_hours = asNumber(argsObj.estimated_hours); if ('done_ratio' in argsObj) issueData.done_ratio = asNumber(argsObj.done_ratio); const response = await client.performRequest('issues.json', { method: 'POST', body: JSON.stringify({ issue: issueData }) }); return { content: [{ type: "text", text: `Issue created successfully:\n\n` + JSON.stringify(response.issue, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: `Error creating issue: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }, /** * Update an existing issue */ update_issue: async (args) => { try { if (typeof args !== 'object' || args === null) { throw new ValidationError("Arguments must be an object"); } const argsObj = args; const issueId = asNumber(argsObj.issue_id); const issueData = {}; if ('project_id' in argsObj) issueData.project = { id: asNumber(argsObj.project_id) }; if ('tracker_id' in argsObj) issueData.tracker = { id: asNumber(argsObj.tracker_id) }; if ('status_id' in argsObj) issueData.status = { id: asNumber(argsObj.status_id) }; if ('priority_id' in argsObj) issueData.priority = { id: asNumber(argsObj.priority_id) }; if ('subject' in argsObj) issueData.subject = String(argsObj.subject); if ('description' in argsObj) issueData.description = String(argsObj.description); if ('assigned_to_id' in argsObj) issueData.assigned_to = { id: asNumber(argsObj.assigned_to_id) }; if ('start_date' in argsObj) issueData.start_date = String(argsObj.start_date); if ('due_date' in argsObj) issueData.due_date = String(argsObj.due_date); if ('estimated_hours' in argsObj) issueData.estimated_hours = asNumber(argsObj.estimated_hours); if ('done_ratio' in argsObj) issueData.done_ratio = asNumber(argsObj.done_ratio); const updateData = { issue: issueData }; if ('notes' in argsObj) updateData.issue.notes = String(argsObj.notes); await client.performRequest(`issues/${issueId}.json`, { method: 'PUT', body: JSON.stringify(updateData) }); return { content: [{ type: "text", text: `Issue #${issueId} updated successfully` }] }; } catch (error) { return { content: [{ type: "text", text: `Error updating issue: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }, /** * Delete an issue */ delete_issue: async (args) => { try { if (typeof args !== 'object' || args === null) { throw new ValidationError("Arguments must be an object"); } const argsObj = args; const issueId = asNumber(argsObj.issue_id); await client.performRequest(`issues/${issueId}.json`, { method: 'DELETE' }); return { content: [{ type: "text", text: `Issue #${issueId} deleted successfully` }] }; } catch (error) { return { content: [{ type: "text", text: `Error deleting issue: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }, /** * Add a watcher to an issue */ add_issue_watcher: async (args) => { try { if (typeof args !== 'object' || args === null) { throw new ValidationError("Arguments must be an object"); } const argsObj = args; const issueId = asNumber(argsObj.issue_id); const userId = asNumber(argsObj.user_id); await client.performRequest(`issues/${issueId}/watchers.json`, { method: 'POST', body: JSON.stringify({ user_id: userId }) }); return { content: [{ type: "text", text: `User ${userId} added as watcher to issue #${issueId}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error adding watcher: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }, /** * Remove a watcher from an issue */ remove_issue_watcher: async (args) => { try { if (typeof args !== 'object' || args === null) { throw new ValidationError("Arguments must be an object"); } const argsObj = args; const issueId = asNumber(argsObj.issue_id); const userId = asNumber(argsObj.user_id); await client.performRequest(`issues/${issueId}/watchers/${userId}.json`, { method: 'DELETE' }); return { content: [{ type: "text", text: `User ${userId} removed as watcher from issue #${issueId}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error removing watcher: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }, }; }