@pschroee/redmine-mcp 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/formatters/index.d.ts +2 -0
- package/dist/formatters/index.js +2 -0
- package/dist/formatters/issue.d.ts +11 -0
- package/dist/formatters/issue.js +128 -0
- package/dist/formatters/journals.d.ts +5 -0
- package/dist/formatters/journals.js +120 -0
- package/dist/server.js +1 -1
- package/dist/tools/checklists.js +5 -5
- package/dist/tools/core.js +10 -2
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -331,11 +331,11 @@ Add to your Claude Desktop config (`%APPDATA%\Claude\claude_desktop_config.json`
|
|
|
331
331
|
|
|
332
332
|
> Requires [redmine_checklists](https://www.redmineup.com/pages/plugins/checklists) plugin
|
|
333
333
|
|
|
334
|
-
- `
|
|
335
|
-
- `
|
|
336
|
-
- `
|
|
337
|
-
- `
|
|
338
|
-
- `
|
|
334
|
+
- `list_checklist_items` - List checklist items for an issue
|
|
335
|
+
- `get_checklist_item` - Get checklist item details
|
|
336
|
+
- `create_checklist_item` - Create checklist item
|
|
337
|
+
- `update_checklist_item` - Update checklist item (text, done status)
|
|
338
|
+
- `delete_checklist_item` - Delete checklist item
|
|
339
339
|
|
|
340
340
|
### Plugin: Agile (plugin_agile)
|
|
341
341
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineIssue } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format an issue as complete Markdown
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatIssue(issue: RedmineIssue): string;
|
|
6
|
+
/**
|
|
7
|
+
* Format an issue API response as Markdown
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatIssueResponse(response: {
|
|
10
|
+
issue: RedmineIssue;
|
|
11
|
+
}): string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { formatJournals } from "./journals.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a date string to readable format
|
|
4
|
+
*/
|
|
5
|
+
function formatDate(isoDate) {
|
|
6
|
+
return new Date(isoDate).toISOString().slice(0, 16).replace("T", " ");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Format an issue as complete Markdown
|
|
10
|
+
*/
|
|
11
|
+
export function formatIssue(issue) {
|
|
12
|
+
const lines = [];
|
|
13
|
+
// Title
|
|
14
|
+
lines.push(`# #${issue.id}: ${issue.subject}`);
|
|
15
|
+
lines.push("");
|
|
16
|
+
// Status line
|
|
17
|
+
const statusParts = [];
|
|
18
|
+
statusParts.push(`**Status:** ${issue.status.name}`);
|
|
19
|
+
statusParts.push(`**Priority:** ${issue.priority.name}`);
|
|
20
|
+
if (issue.tracker) {
|
|
21
|
+
statusParts.push(`**Tracker:** ${issue.tracker.name}`);
|
|
22
|
+
}
|
|
23
|
+
if (issue.assigned_to) {
|
|
24
|
+
statusParts.push(`**Assigned:** ${issue.assigned_to.name}`);
|
|
25
|
+
}
|
|
26
|
+
lines.push(statusParts.join(" | "));
|
|
27
|
+
lines.push("");
|
|
28
|
+
// Metadata table
|
|
29
|
+
lines.push("| Field | Value |");
|
|
30
|
+
lines.push("|-------|-------|");
|
|
31
|
+
lines.push(`| Project | ${issue.project.name} |`);
|
|
32
|
+
lines.push(`| Author | ${issue.author.name} |`);
|
|
33
|
+
lines.push(`| Created | ${formatDate(issue.created_on)} |`);
|
|
34
|
+
lines.push(`| Updated | ${formatDate(issue.updated_on)} |`);
|
|
35
|
+
if (issue.closed_on) {
|
|
36
|
+
lines.push(`| Closed | ${formatDate(issue.closed_on)} |`);
|
|
37
|
+
}
|
|
38
|
+
if (issue.start_date) {
|
|
39
|
+
lines.push(`| Start Date | ${issue.start_date} |`);
|
|
40
|
+
}
|
|
41
|
+
if (issue.due_date) {
|
|
42
|
+
lines.push(`| Due Date | ${issue.due_date} |`);
|
|
43
|
+
}
|
|
44
|
+
if (issue.estimated_hours) {
|
|
45
|
+
lines.push(`| Estimated | ${issue.estimated_hours}h |`);
|
|
46
|
+
}
|
|
47
|
+
if (issue.spent_hours) {
|
|
48
|
+
lines.push(`| Spent | ${issue.spent_hours}h |`);
|
|
49
|
+
}
|
|
50
|
+
if (issue.done_ratio !== undefined) {
|
|
51
|
+
lines.push(`| Progress | ${issue.done_ratio}% |`);
|
|
52
|
+
}
|
|
53
|
+
if (issue.category) {
|
|
54
|
+
lines.push(`| Category | ${issue.category.name} |`);
|
|
55
|
+
}
|
|
56
|
+
if (issue.fixed_version) {
|
|
57
|
+
lines.push(`| Version | ${issue.fixed_version.name} |`);
|
|
58
|
+
}
|
|
59
|
+
if (issue.parent) {
|
|
60
|
+
lines.push(`| Parent | #${issue.parent.id} |`);
|
|
61
|
+
}
|
|
62
|
+
lines.push("");
|
|
63
|
+
// Custom fields
|
|
64
|
+
if (issue.custom_fields && issue.custom_fields.length > 0) {
|
|
65
|
+
lines.push("## Custom Fields");
|
|
66
|
+
lines.push("");
|
|
67
|
+
for (const cf of issue.custom_fields) {
|
|
68
|
+
const value = Array.isArray(cf.value) ? cf.value.join(", ") : cf.value;
|
|
69
|
+
if (value) {
|
|
70
|
+
lines.push(`- **${cf.name}:** ${value}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
}
|
|
75
|
+
// Description
|
|
76
|
+
if (issue.description) {
|
|
77
|
+
lines.push("## Description");
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push(issue.description);
|
|
80
|
+
lines.push("");
|
|
81
|
+
}
|
|
82
|
+
// Children
|
|
83
|
+
if (issue.children && issue.children.length > 0) {
|
|
84
|
+
lines.push("## Subtasks");
|
|
85
|
+
lines.push("");
|
|
86
|
+
for (const child of issue.children) {
|
|
87
|
+
lines.push(`- #${child.id}: ${child.subject} (${child.tracker.name})`);
|
|
88
|
+
}
|
|
89
|
+
lines.push("");
|
|
90
|
+
}
|
|
91
|
+
// Relations
|
|
92
|
+
if (issue.relations && issue.relations.length > 0) {
|
|
93
|
+
lines.push("## Relations");
|
|
94
|
+
lines.push("");
|
|
95
|
+
for (const rel of issue.relations) {
|
|
96
|
+
const target = rel.issue_id === issue.id ? rel.issue_to_id : rel.issue_id;
|
|
97
|
+
lines.push(`- ${rel.relation_type} #${target}`);
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
// Attachments
|
|
102
|
+
if (issue.attachments && issue.attachments.length > 0) {
|
|
103
|
+
lines.push("## Attachments");
|
|
104
|
+
lines.push("");
|
|
105
|
+
for (const att of issue.attachments) {
|
|
106
|
+
lines.push(`- [${att.filename}](${att.content_url}) (${att.filesize} bytes)`);
|
|
107
|
+
}
|
|
108
|
+
lines.push("");
|
|
109
|
+
}
|
|
110
|
+
// Watchers
|
|
111
|
+
if (issue.watchers && issue.watchers.length > 0) {
|
|
112
|
+
lines.push("## Watchers");
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(issue.watchers.map(w => w.name).join(", "));
|
|
115
|
+
lines.push("");
|
|
116
|
+
}
|
|
117
|
+
// Journals (history)
|
|
118
|
+
if (issue.journals && issue.journals.length > 0) {
|
|
119
|
+
lines.push(formatJournals(issue.journals));
|
|
120
|
+
}
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Format an issue API response as Markdown
|
|
125
|
+
*/
|
|
126
|
+
export function formatIssueResponse(response) {
|
|
127
|
+
return formatIssue(response.issue);
|
|
128
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { diffLines } from "diff";
|
|
2
|
+
/**
|
|
3
|
+
* Fields that contain large text and should use diff formatting
|
|
4
|
+
*/
|
|
5
|
+
const LARGE_TEXT_FIELDS = ["description"];
|
|
6
|
+
/**
|
|
7
|
+
* Format a date string to a readable format
|
|
8
|
+
*/
|
|
9
|
+
function formatDate(isoDate) {
|
|
10
|
+
const date = new Date(isoDate);
|
|
11
|
+
return date.toISOString().slice(0, 16).replace("T", " ");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Generate a unified diff for large text changes
|
|
15
|
+
*/
|
|
16
|
+
function generateDiff(oldValue, newValue) {
|
|
17
|
+
const changes = diffLines(oldValue || "", newValue || "");
|
|
18
|
+
const lines = [];
|
|
19
|
+
for (const change of changes) {
|
|
20
|
+
const prefix = change.added ? "+" : change.removed ? "-" : " ";
|
|
21
|
+
const text = change.value.replace(/\n$/, "");
|
|
22
|
+
// Skip unchanged parts that are too long (context)
|
|
23
|
+
if (!change.added && !change.removed && text.length > 200) {
|
|
24
|
+
lines.push(" [...]");
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
for (const line of text.split("\n")) {
|
|
28
|
+
if (line || change.added || change.removed) {
|
|
29
|
+
lines.push(`${prefix} ${line}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return lines.join("\n");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Format a single journal detail (field change)
|
|
37
|
+
*/
|
|
38
|
+
function formatDetail(detail) {
|
|
39
|
+
const { property, name, old_value, new_value } = detail;
|
|
40
|
+
// Handle attachment additions/removals
|
|
41
|
+
if (property === "attachment") {
|
|
42
|
+
if (new_value && !old_value) {
|
|
43
|
+
return `- Added attachment: ${new_value}`;
|
|
44
|
+
}
|
|
45
|
+
if (old_value && !new_value) {
|
|
46
|
+
return `- Removed attachment: ${old_value}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Handle relation changes
|
|
50
|
+
if (property === "relation") {
|
|
51
|
+
if (new_value && !old_value) {
|
|
52
|
+
return `- Added relation: ${name} → #${new_value}`;
|
|
53
|
+
}
|
|
54
|
+
if (old_value && !new_value) {
|
|
55
|
+
return `- Removed relation: ${name} → #${old_value}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Handle custom fields
|
|
59
|
+
if (property === "cf") {
|
|
60
|
+
const fieldName = name;
|
|
61
|
+
if (!old_value && new_value) {
|
|
62
|
+
return `- ${fieldName}: _(empty)_ → ${new_value}`;
|
|
63
|
+
}
|
|
64
|
+
if (old_value && !new_value) {
|
|
65
|
+
return `- ${fieldName}: ${old_value} → _(empty)_`;
|
|
66
|
+
}
|
|
67
|
+
return `- ${fieldName}: ${old_value} → ${new_value}`;
|
|
68
|
+
}
|
|
69
|
+
// Handle attribute changes
|
|
70
|
+
if (property === "attr") {
|
|
71
|
+
// Large text fields get diff formatting
|
|
72
|
+
if (LARGE_TEXT_FIELDS.includes(name) && old_value && new_value) {
|
|
73
|
+
const diff = generateDiff(old_value, new_value);
|
|
74
|
+
return `- ${name}:\n\`\`\`diff\n${diff}\n\`\`\``;
|
|
75
|
+
}
|
|
76
|
+
// Simple field changes
|
|
77
|
+
const oldDisplay = old_value || "_(empty)_";
|
|
78
|
+
const newDisplay = new_value || "_(empty)_";
|
|
79
|
+
return `- ${name}: ${oldDisplay} → ${newDisplay}`;
|
|
80
|
+
}
|
|
81
|
+
// Fallback for unknown property types
|
|
82
|
+
return `- ${property}.${name}: ${old_value || "_(empty)_"} → ${new_value || "_(empty)_"}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Format a single journal entry as Markdown
|
|
86
|
+
*/
|
|
87
|
+
function formatJournalEntry(journal) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
// Header with date and user
|
|
90
|
+
const date = formatDate(journal.created_on);
|
|
91
|
+
const user = journal.user.name;
|
|
92
|
+
const privateTag = journal.private_notes ? " 🔒" : "";
|
|
93
|
+
lines.push(`### ${date} - ${user}${privateTag}`);
|
|
94
|
+
lines.push("");
|
|
95
|
+
// Notes (if any)
|
|
96
|
+
if (journal.notes && journal.notes.trim()) {
|
|
97
|
+
lines.push(journal.notes.trim());
|
|
98
|
+
lines.push("");
|
|
99
|
+
}
|
|
100
|
+
// Details (field changes)
|
|
101
|
+
if (journal.details && journal.details.length > 0) {
|
|
102
|
+
lines.push("**Changes:**");
|
|
103
|
+
for (const detail of journal.details) {
|
|
104
|
+
lines.push(formatDetail(detail));
|
|
105
|
+
}
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Format an array of journal entries as Markdown
|
|
112
|
+
*/
|
|
113
|
+
export function formatJournals(journals) {
|
|
114
|
+
if (!journals || journals.length === 0) {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
const header = `## History (${journals.length} entries)\n\n`;
|
|
118
|
+
const entries = journals.map(formatJournalEntry).join("\n---\n\n");
|
|
119
|
+
return header + entries;
|
|
120
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { registerTools } from "./tools/index.js";
|
|
|
3
3
|
export function createServer(redmineClient, toolGroups) {
|
|
4
4
|
const server = new McpServer({
|
|
5
5
|
name: "redmine-mcp",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.3",
|
|
7
7
|
});
|
|
8
8
|
registerTools(server, redmineClient, toolGroups);
|
|
9
9
|
return server;
|
package/dist/tools/checklists.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export function registerChecklistsTools(server, client) {
|
|
3
|
-
server.registerTool("
|
|
3
|
+
server.registerTool("list_checklist_items", {
|
|
4
4
|
description: "List all checklist items for an issue (requires redmine_checklists plugin)",
|
|
5
5
|
inputSchema: {
|
|
6
6
|
issue_id: z.number().describe("The issue ID to get checklists for"),
|
|
@@ -11,7 +11,7 @@ export function registerChecklistsTools(server, client) {
|
|
|
11
11
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
12
12
|
};
|
|
13
13
|
});
|
|
14
|
-
server.registerTool("
|
|
14
|
+
server.registerTool("get_checklist_item", {
|
|
15
15
|
description: "Get a specific checklist item by ID (requires redmine_checklists plugin)",
|
|
16
16
|
inputSchema: {
|
|
17
17
|
checklist_id: z.number().describe("The checklist item ID"),
|
|
@@ -22,7 +22,7 @@ export function registerChecklistsTools(server, client) {
|
|
|
22
22
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
23
23
|
};
|
|
24
24
|
});
|
|
25
|
-
server.registerTool("
|
|
25
|
+
server.registerTool("create_checklist_item", {
|
|
26
26
|
description: "Create a new checklist item for an issue (requires redmine_checklists plugin)",
|
|
27
27
|
inputSchema: {
|
|
28
28
|
issue_id: z.number().describe("The issue ID to add the checklist item to"),
|
|
@@ -37,7 +37,7 @@ export function registerChecklistsTools(server, client) {
|
|
|
37
37
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
|
-
server.registerTool("
|
|
40
|
+
server.registerTool("update_checklist_item", {
|
|
41
41
|
description: "Update an existing checklist item (requires redmine_checklists plugin)",
|
|
42
42
|
inputSchema: {
|
|
43
43
|
checklist_id: z.number().describe("The checklist item ID to update"),
|
|
@@ -52,7 +52,7 @@ export function registerChecklistsTools(server, client) {
|
|
|
52
52
|
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
53
53
|
};
|
|
54
54
|
});
|
|
55
|
-
server.registerTool("
|
|
55
|
+
server.registerTool("delete_checklist_item", {
|
|
56
56
|
description: "Delete a checklist item (requires redmine_checklists plugin)",
|
|
57
57
|
inputSchema: {
|
|
58
58
|
checklist_id: z.number().describe("The checklist item ID to delete"),
|
package/dist/tools/core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatIssueResponse } from "../formatters/index.js";
|
|
2
3
|
export function registerCoreTools(server, client) {
|
|
3
4
|
// === ISSUES ===
|
|
4
5
|
server.registerTool("list_issues", {
|
|
@@ -28,15 +29,22 @@ export function registerCoreTools(server, client) {
|
|
|
28
29
|
};
|
|
29
30
|
});
|
|
30
31
|
server.registerTool("get_issue", {
|
|
31
|
-
description: "Get details of a specific issue by ID",
|
|
32
|
+
description: "Get details of a specific issue by ID. Returns Markdown-formatted response.",
|
|
32
33
|
inputSchema: {
|
|
33
34
|
issue_id: z.number().describe("The issue ID"),
|
|
34
35
|
include: z.string().optional().describe("Include: attachments, relations, journals, watchers, children, changesets, allowed_statuses"),
|
|
35
36
|
},
|
|
36
37
|
}, async (params) => {
|
|
37
38
|
const result = await client.getIssue(params.issue_id, params.include);
|
|
39
|
+
// Check if this is an error response
|
|
40
|
+
if ("error" in result) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Format response as Markdown
|
|
38
46
|
return {
|
|
39
|
-
content: [{ type: "text", text:
|
|
47
|
+
content: [{ type: "text", text: formatIssueResponse(result) }],
|
|
40
48
|
};
|
|
41
49
|
});
|
|
42
50
|
server.registerTool("create_issue", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pschroee/redmine-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "MCP server for Redmine - full API access with configurable tool groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,10 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
44
|
+
"diff": "^8.0.2",
|
|
44
45
|
"zod": "^3.23.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@eslint/js": "^9.39.2",
|
|
49
|
+
"@types/diff": "^7.0.2",
|
|
48
50
|
"@types/node": "^20.0.0",
|
|
49
51
|
"dotenv": "^17.2.3",
|
|
50
52
|
"eslint": "^9.39.2",
|