@pschroee/redmine-mcp 0.4.2 → 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/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/core.js +10 -2
- package/package.json +3 -1
|
@@ -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/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",
|