@pschroee/redmine-mcp 0.4.3 → 0.4.4

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;
@@ -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,19 @@ 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
+ * Field name mappings for display (remove _id suffix)
8
+ */
9
+ const FIELD_DISPLAY_NAMES = {
10
+ status_id: "status",
11
+ tracker_id: "tracker",
12
+ priority_id: "priority",
13
+ assigned_to_id: "assigned_to",
14
+ category_id: "category",
15
+ fixed_version_id: "version",
16
+ parent_id: "parent",
17
+ project_id: "project",
18
+ };
6
19
  /**
7
20
  * Format a date string to a readable format
8
21
  */
@@ -32,10 +45,26 @@ function generateDiff(oldValue, newValue) {
32
45
  }
33
46
  return lines.join("\n");
34
47
  }
48
+ /**
49
+ * Resolve an ID value to a display string with name
50
+ */
51
+ function resolveValue(fieldName, value, lookup) {
52
+ if (!value)
53
+ return "_(empty)_";
54
+ const fieldLookup = lookup[fieldName];
55
+ if (fieldLookup && fieldLookup[value]) {
56
+ return `${fieldLookup[value]} (${value})`;
57
+ }
58
+ // For parent_id, show as issue reference
59
+ if (fieldName === "parent_id") {
60
+ return `#${value}`;
61
+ }
62
+ return value;
63
+ }
35
64
  /**
36
65
  * Format a single journal detail (field change)
37
66
  */
38
- function formatDetail(detail) {
67
+ function formatDetail(detail, lookup) {
39
68
  const { property, name, old_value, new_value } = detail;
40
69
  // Handle attachment additions/removals
41
70
  if (property === "attachment") {
@@ -73,10 +102,18 @@ function formatDetail(detail) {
73
102
  const diff = generateDiff(old_value, new_value);
74
103
  return `- ${name}:\n\`\`\`diff\n${diff}\n\`\`\``;
75
104
  }
105
+ // Get display name for the field
106
+ const displayName = FIELD_DISPLAY_NAMES[name] || name;
107
+ // Resolve ID fields to names
108
+ if (name.endsWith("_id") && lookup[name]) {
109
+ const oldDisplay = resolveValue(name, old_value, lookup);
110
+ const newDisplay = resolveValue(name, new_value, lookup);
111
+ return `- ${displayName}: ${oldDisplay} → ${newDisplay}`;
112
+ }
76
113
  // Simple field changes
77
114
  const oldDisplay = old_value || "_(empty)_";
78
115
  const newDisplay = new_value || "_(empty)_";
79
- return `- ${name}: ${oldDisplay} → ${newDisplay}`;
116
+ return `- ${displayName}: ${oldDisplay} → ${newDisplay}`;
80
117
  }
81
118
  // Fallback for unknown property types
82
119
  return `- ${property}.${name}: ${old_value || "_(empty)_"} → ${new_value || "_(empty)_"}`;
@@ -84,7 +121,7 @@ function formatDetail(detail) {
84
121
  /**
85
122
  * Format a single journal entry as Markdown
86
123
  */
87
- function formatJournalEntry(journal) {
124
+ function formatJournalEntry(journal, lookup) {
88
125
  const lines = [];
89
126
  // Header with date and user
90
127
  const date = formatDate(journal.created_on);
@@ -101,7 +138,7 @@ function formatJournalEntry(journal) {
101
138
  if (journal.details && journal.details.length > 0) {
102
139
  lines.push("**Changes:**");
103
140
  for (const detail of journal.details) {
104
- lines.push(formatDetail(detail));
141
+ lines.push(formatDetail(detail, lookup));
105
142
  }
106
143
  lines.push("");
107
144
  }
@@ -110,11 +147,11 @@ function formatJournalEntry(journal) {
110
147
  /**
111
148
  * Format an array of journal entries as Markdown
112
149
  */
113
- export function formatJournals(journals) {
150
+ export function formatJournals(journals, lookup = {}) {
114
151
  if (!journals || journals.length === 0) {
115
152
  return "";
116
153
  }
117
154
  const header = `## History (${journals.length} entries)\n\n`;
118
- const entries = journals.map(formatJournalEntry).join("\n---\n\n");
155
+ const entries = journals.map(j => formatJournalEntry(j, lookup)).join("\n---\n\n");
119
156
  return header + entries;
120
157
  }
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.3",
6
+ version: "0.4.4",
7
7
  });
8
8
  registerTools(server, redmineClient, toolGroups);
9
9
  return server;
@@ -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
- const result = await client.getIssue(params.issue_id, params.include);
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", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pschroee/redmine-mcp",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "MCP server for Redmine - full API access with configurable tool groups",
5
5
  "type": "module",
6
6
  "bin": {