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
JavaScript
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
};
}
},
};
}