@pschroee/redmine-mcp 0.4.3 → 0.4.5
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.
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { formatJournals } from "./journals.js";
|
|
1
|
+
export { formatJournals, type NameLookup } from "./journals.js";
|
|
2
2
|
export { formatIssue, formatIssueResponse } from "./issue.js";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { RedmineIssue } from "../redmine/types.js";
|
|
2
|
+
import { type NameLookup } from "./journals.js";
|
|
2
3
|
/**
|
|
3
4
|
* Format an issue as complete Markdown
|
|
4
5
|
*/
|
|
5
|
-
export declare function formatIssue(issue: RedmineIssue): string;
|
|
6
|
+
export declare function formatIssue(issue: RedmineIssue, lookup?: NameLookup): string;
|
|
6
7
|
/**
|
|
7
8
|
* Format an issue API response as Markdown
|
|
8
9
|
*/
|
|
9
10
|
export declare function formatIssueResponse(response: {
|
|
10
11
|
issue: RedmineIssue;
|
|
11
|
-
}): string;
|
|
12
|
+
}, lookup?: NameLookup): string;
|
package/dist/formatters/issue.js
CHANGED
|
@@ -8,7 +8,7 @@ function formatDate(isoDate) {
|
|
|
8
8
|
/**
|
|
9
9
|
* Format an issue as complete Markdown
|
|
10
10
|
*/
|
|
11
|
-
export function formatIssue(issue) {
|
|
11
|
+
export function formatIssue(issue, lookup = {}) {
|
|
12
12
|
const lines = [];
|
|
13
13
|
// Title
|
|
14
14
|
lines.push(`# #${issue.id}: ${issue.subject}`);
|
|
@@ -116,13 +116,13 @@ export function formatIssue(issue) {
|
|
|
116
116
|
}
|
|
117
117
|
// Journals (history)
|
|
118
118
|
if (issue.journals && issue.journals.length > 0) {
|
|
119
|
-
lines.push(formatJournals(issue.journals));
|
|
119
|
+
lines.push(formatJournals(issue.journals, lookup));
|
|
120
120
|
}
|
|
121
121
|
return lines.join("\n");
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
124
|
* Format an issue API response as Markdown
|
|
125
125
|
*/
|
|
126
|
-
export function formatIssueResponse(response) {
|
|
127
|
-
return formatIssue(response.issue);
|
|
126
|
+
export function formatIssueResponse(response, lookup = {}) {
|
|
127
|
+
return formatIssue(response.issue, lookup);
|
|
128
128
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { RedmineJournal } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Lookup map for resolving IDs to names
|
|
4
|
+
* Key: field name (e.g., "status_id"), Value: map of ID -> name
|
|
5
|
+
*/
|
|
6
|
+
export type NameLookup = Record<string, Record<string, string>>;
|
|
2
7
|
/**
|
|
3
8
|
* Format an array of journal entries as Markdown
|
|
4
9
|
*/
|
|
5
|
-
export declare function formatJournals(journals: RedmineJournal[]): string;
|
|
10
|
+
export declare function formatJournals(journals: RedmineJournal[], lookup?: NameLookup): string;
|
|
@@ -3,6 +3,71 @@ import { diffLines } from "diff";
|
|
|
3
3
|
* Fields that contain large text and should use diff formatting
|
|
4
4
|
*/
|
|
5
5
|
const LARGE_TEXT_FIELDS = ["description"];
|
|
6
|
+
/**
|
|
7
|
+
* Format checklist changes as a readable diff
|
|
8
|
+
* Only shows items that changed between old and new state
|
|
9
|
+
*/
|
|
10
|
+
function formatChecklistDiff(oldValue, newValue) {
|
|
11
|
+
let oldItems = [];
|
|
12
|
+
let newItems = [];
|
|
13
|
+
try {
|
|
14
|
+
oldItems = JSON.parse(oldValue || "[]");
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Invalid JSON, fall back to raw display
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
newItems = JSON.parse(newValue || "[]");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// Create maps for easy lookup by subject (more reliable than ID for diffing)
|
|
27
|
+
const oldBySubject = new Map(oldItems.map(item => [item.subject, item]));
|
|
28
|
+
const newBySubject = new Map(newItems.map(item => [item.subject, item]));
|
|
29
|
+
const changes = [];
|
|
30
|
+
// Find changed and added items
|
|
31
|
+
for (const newItem of newItems) {
|
|
32
|
+
const oldItem = oldBySubject.get(newItem.subject);
|
|
33
|
+
if (!oldItem) {
|
|
34
|
+
// New item added
|
|
35
|
+
const status = newItem.is_done ? "✅" : "❌";
|
|
36
|
+
changes.push(` - ➕ ${newItem.subject}: ${status}`);
|
|
37
|
+
}
|
|
38
|
+
else if (oldItem.is_done !== newItem.is_done) {
|
|
39
|
+
// Status changed
|
|
40
|
+
const oldStatus = oldItem.is_done ? "✅" : "❌";
|
|
41
|
+
const newStatus = newItem.is_done ? "✅" : "❌";
|
|
42
|
+
changes.push(` - ${newItem.subject}: ${oldStatus} → ${newStatus}`);
|
|
43
|
+
}
|
|
44
|
+
// If subject and status are same, no change to report
|
|
45
|
+
}
|
|
46
|
+
// Find removed items
|
|
47
|
+
for (const oldItem of oldItems) {
|
|
48
|
+
if (!newBySubject.has(oldItem.subject)) {
|
|
49
|
+
const status = oldItem.is_done ? "✅" : "❌";
|
|
50
|
+
changes.push(` - ➖ ${oldItem.subject}: ${status}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (changes.length === 0) {
|
|
54
|
+
return null; // No meaningful changes
|
|
55
|
+
}
|
|
56
|
+
return changes.join("\n");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Field name mappings for display (remove _id suffix)
|
|
60
|
+
*/
|
|
61
|
+
const FIELD_DISPLAY_NAMES = {
|
|
62
|
+
status_id: "status",
|
|
63
|
+
tracker_id: "tracker",
|
|
64
|
+
priority_id: "priority",
|
|
65
|
+
assigned_to_id: "assigned_to",
|
|
66
|
+
category_id: "category",
|
|
67
|
+
fixed_version_id: "version",
|
|
68
|
+
parent_id: "parent",
|
|
69
|
+
project_id: "project",
|
|
70
|
+
};
|
|
6
71
|
/**
|
|
7
72
|
* Format a date string to a readable format
|
|
8
73
|
*/
|
|
@@ -32,10 +97,26 @@ function generateDiff(oldValue, newValue) {
|
|
|
32
97
|
}
|
|
33
98
|
return lines.join("\n");
|
|
34
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve an ID value to a display string with name
|
|
102
|
+
*/
|
|
103
|
+
function resolveValue(fieldName, value, lookup) {
|
|
104
|
+
if (!value)
|
|
105
|
+
return "_(empty)_";
|
|
106
|
+
const fieldLookup = lookup[fieldName];
|
|
107
|
+
if (fieldLookup && fieldLookup[value]) {
|
|
108
|
+
return `${fieldLookup[value]} (${value})`;
|
|
109
|
+
}
|
|
110
|
+
// For parent_id, show as issue reference
|
|
111
|
+
if (fieldName === "parent_id") {
|
|
112
|
+
return `#${value}`;
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
35
116
|
/**
|
|
36
117
|
* Format a single journal detail (field change)
|
|
37
118
|
*/
|
|
38
|
-
function formatDetail(detail) {
|
|
119
|
+
function formatDetail(detail, lookup) {
|
|
39
120
|
const { property, name, old_value, new_value } = detail;
|
|
40
121
|
// Handle attachment additions/removals
|
|
41
122
|
if (property === "attachment") {
|
|
@@ -68,15 +149,31 @@ function formatDetail(detail) {
|
|
|
68
149
|
}
|
|
69
150
|
// Handle attribute changes
|
|
70
151
|
if (property === "attr") {
|
|
152
|
+
// Checklist changes get special formatting
|
|
153
|
+
if (name === "checklist") {
|
|
154
|
+
const checklistDiff = formatChecklistDiff(old_value || "", new_value || "");
|
|
155
|
+
if (checklistDiff) {
|
|
156
|
+
return `- checklist:\n${checklistDiff}`;
|
|
157
|
+
}
|
|
158
|
+
// Fall through to default formatting if parsing failed
|
|
159
|
+
}
|
|
71
160
|
// Large text fields get diff formatting
|
|
72
161
|
if (LARGE_TEXT_FIELDS.includes(name) && old_value && new_value) {
|
|
73
162
|
const diff = generateDiff(old_value, new_value);
|
|
74
163
|
return `- ${name}:\n\`\`\`diff\n${diff}\n\`\`\``;
|
|
75
164
|
}
|
|
165
|
+
// Get display name for the field
|
|
166
|
+
const displayName = FIELD_DISPLAY_NAMES[name] || name;
|
|
167
|
+
// Resolve ID fields to names
|
|
168
|
+
if (name.endsWith("_id") && lookup[name]) {
|
|
169
|
+
const oldDisplay = resolveValue(name, old_value, lookup);
|
|
170
|
+
const newDisplay = resolveValue(name, new_value, lookup);
|
|
171
|
+
return `- ${displayName}: ${oldDisplay} → ${newDisplay}`;
|
|
172
|
+
}
|
|
76
173
|
// Simple field changes
|
|
77
174
|
const oldDisplay = old_value || "_(empty)_";
|
|
78
175
|
const newDisplay = new_value || "_(empty)_";
|
|
79
|
-
return `- ${
|
|
176
|
+
return `- ${displayName}: ${oldDisplay} → ${newDisplay}`;
|
|
80
177
|
}
|
|
81
178
|
// Fallback for unknown property types
|
|
82
179
|
return `- ${property}.${name}: ${old_value || "_(empty)_"} → ${new_value || "_(empty)_"}`;
|
|
@@ -84,7 +181,7 @@ function formatDetail(detail) {
|
|
|
84
181
|
/**
|
|
85
182
|
* Format a single journal entry as Markdown
|
|
86
183
|
*/
|
|
87
|
-
function formatJournalEntry(journal) {
|
|
184
|
+
function formatJournalEntry(journal, lookup) {
|
|
88
185
|
const lines = [];
|
|
89
186
|
// Header with date and user
|
|
90
187
|
const date = formatDate(journal.created_on);
|
|
@@ -101,7 +198,7 @@ function formatJournalEntry(journal) {
|
|
|
101
198
|
if (journal.details && journal.details.length > 0) {
|
|
102
199
|
lines.push("**Changes:**");
|
|
103
200
|
for (const detail of journal.details) {
|
|
104
|
-
lines.push(formatDetail(detail));
|
|
201
|
+
lines.push(formatDetail(detail, lookup));
|
|
105
202
|
}
|
|
106
203
|
lines.push("");
|
|
107
204
|
}
|
|
@@ -110,11 +207,11 @@ function formatJournalEntry(journal) {
|
|
|
110
207
|
/**
|
|
111
208
|
* Format an array of journal entries as Markdown
|
|
112
209
|
*/
|
|
113
|
-
export function formatJournals(journals) {
|
|
210
|
+
export function formatJournals(journals, lookup = {}) {
|
|
114
211
|
if (!journals || journals.length === 0) {
|
|
115
212
|
return "";
|
|
116
213
|
}
|
|
117
214
|
const header = `## History (${journals.length} entries)\n\n`;
|
|
118
|
-
const entries = journals.map(formatJournalEntry).join("\n---\n\n");
|
|
215
|
+
const entries = journals.map(j => formatJournalEntry(j, lookup)).join("\n---\n\n");
|
|
119
216
|
return header + entries;
|
|
120
217
|
}
|
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.5",
|
|
7
7
|
});
|
|
8
8
|
registerTools(server, redmineClient, toolGroups);
|
|
9
9
|
return server;
|
package/dist/tools/core.js
CHANGED
|
@@ -35,16 +35,43 @@ export function registerCoreTools(server, client) {
|
|
|
35
35
|
include: z.string().optional().describe("Include: attachments, relations, journals, watchers, children, changesets, allowed_statuses"),
|
|
36
36
|
},
|
|
37
37
|
}, async (params) => {
|
|
38
|
-
|
|
38
|
+
// Fetch issue and enumerations in parallel
|
|
39
|
+
const [result, statusesResult, trackersResult, prioritiesResult] = await Promise.all([
|
|
40
|
+
client.getIssue(params.issue_id, params.include),
|
|
41
|
+
client.listIssueStatuses(),
|
|
42
|
+
client.listTrackers(),
|
|
43
|
+
client.listIssuePriorities(),
|
|
44
|
+
]);
|
|
39
45
|
// Check if this is an error response
|
|
40
46
|
if ("error" in result) {
|
|
41
47
|
return {
|
|
42
48
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
43
49
|
};
|
|
44
50
|
}
|
|
51
|
+
// Build lookup map for ID -> name resolution
|
|
52
|
+
const lookup = {
|
|
53
|
+
status_id: {},
|
|
54
|
+
tracker_id: {},
|
|
55
|
+
priority_id: {},
|
|
56
|
+
};
|
|
57
|
+
if (!("error" in statusesResult)) {
|
|
58
|
+
for (const s of statusesResult.issue_statuses) {
|
|
59
|
+
lookup.status_id[String(s.id)] = s.name;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!("error" in trackersResult)) {
|
|
63
|
+
for (const t of trackersResult.trackers) {
|
|
64
|
+
lookup.tracker_id[String(t.id)] = t.name;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!("error" in prioritiesResult)) {
|
|
68
|
+
for (const p of prioritiesResult.issue_priorities) {
|
|
69
|
+
lookup.priority_id[String(p.id)] = p.name;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
45
72
|
// Format response as Markdown
|
|
46
73
|
return {
|
|
47
|
-
content: [{ type: "text", text: formatIssueResponse(result) }],
|
|
74
|
+
content: [{ type: "text", text: formatIssueResponse(result, lookup) }],
|
|
48
75
|
};
|
|
49
76
|
});
|
|
50
77
|
server.registerTool("create_issue", {
|