@pschroee/redmine-mcp 0.5.0 → 0.5.2

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.
Files changed (39) hide show
  1. package/dist/formatters/account.d.ts +2 -0
  2. package/dist/formatters/account.js +48 -0
  3. package/dist/formatters/agile.d.ts +6 -0
  4. package/dist/formatters/agile.js +53 -0
  5. package/dist/formatters/checklist.d.ts +5 -0
  6. package/dist/formatters/checklist.js +30 -0
  7. package/dist/formatters/file.d.ts +5 -0
  8. package/dist/formatters/file.js +61 -0
  9. package/dist/formatters/index.d.ts +7 -1
  10. package/dist/formatters/index.js +7 -1
  11. package/dist/formatters/issue.d.ts +8 -2
  12. package/dist/formatters/issue.js +7 -16
  13. package/dist/formatters/journals.d.ts +9 -1
  14. package/dist/formatters/journals.js +17 -15
  15. package/dist/formatters/membership.d.ts +5 -0
  16. package/dist/formatters/membership.js +41 -0
  17. package/dist/formatters/metadata.d.ts +47 -0
  18. package/dist/formatters/metadata.js +76 -0
  19. package/dist/formatters/project.js +1 -6
  20. package/dist/formatters/relation.d.ts +5 -0
  21. package/dist/formatters/relation.js +55 -0
  22. package/dist/formatters/search.js +1 -15
  23. package/dist/formatters/user.js +1 -6
  24. package/dist/formatters/utils.d.ts +12 -0
  25. package/dist/formatters/utils.js +31 -0
  26. package/dist/formatters/wiki.js +1 -6
  27. package/dist/redmine/client.d.ts +4 -0
  28. package/dist/redmine/client.js +6 -0
  29. package/dist/server.js +1 -1
  30. package/dist/tools/account.js +7 -1
  31. package/dist/tools/agile.js +19 -3
  32. package/dist/tools/checklists.js +13 -2
  33. package/dist/tools/core.js +4 -1
  34. package/dist/tools/enumerations.js +7 -2
  35. package/dist/tools/files.js +13 -2
  36. package/dist/tools/memberships.js +13 -2
  37. package/dist/tools/metadata.js +19 -4
  38. package/dist/tools/relations.js +13 -3
  39. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ import type { RedmineMyAccountResponse } from "../redmine/types.js";
2
+ export declare function formatMyAccount(response: RedmineMyAccountResponse): string;
@@ -0,0 +1,48 @@
1
+ import { formatDate } from "./utils.js";
2
+ const USER_STATUS = {
3
+ 1: "Active",
4
+ 2: "Registered",
5
+ 3: "Locked",
6
+ };
7
+ export function formatMyAccount(response) {
8
+ const user = response.user;
9
+ const lines = [];
10
+ lines.push(`# ${user.firstname} ${user.lastname}`);
11
+ lines.push("");
12
+ const statusParts = [];
13
+ statusParts.push(`**Login:** ${user.login}`);
14
+ if (user.status !== undefined) {
15
+ statusParts.push(`**Status:** ${USER_STATUS[user.status] || "Unknown"}`);
16
+ }
17
+ if (user.admin) {
18
+ statusParts.push(`**Role:** Admin`);
19
+ }
20
+ lines.push(statusParts.join(" | "));
21
+ lines.push("");
22
+ lines.push("| Field | Value |");
23
+ lines.push("|-------|-------|");
24
+ lines.push(`| Email | ${user.mail} |`);
25
+ lines.push(`| Created | ${formatDate(user.created_on)} |`);
26
+ if (user.last_login_on) {
27
+ lines.push(`| Last Login | ${formatDate(user.last_login_on)} |`);
28
+ }
29
+ if (user.twofa_scheme) {
30
+ lines.push(`| 2FA | ${user.twofa_scheme} |`);
31
+ }
32
+ if (user.api_key) {
33
+ lines.push(`| API Key | ${user.api_key} |`);
34
+ }
35
+ lines.push("");
36
+ if (user.custom_fields && user.custom_fields.length > 0) {
37
+ lines.push("## Custom Fields");
38
+ lines.push("");
39
+ lines.push("| Field | Value |");
40
+ lines.push("|-------|-------|");
41
+ for (const cf of user.custom_fields) {
42
+ const value = Array.isArray(cf.value) ? cf.value.join(", ") : cf.value;
43
+ lines.push(`| ${cf.name} | ${value} |`);
44
+ }
45
+ lines.push("");
46
+ }
47
+ return lines.join("\n").trimEnd();
48
+ }
@@ -0,0 +1,6 @@
1
+ import type { RedmineAgileSprint, RedmineAgileSprintsResponse, RedmineAgileDataResponse } from "../redmine/types.js";
2
+ export declare function formatSprint(response: {
3
+ agile_sprint: RedmineAgileSprint;
4
+ }): string;
5
+ export declare function formatSprintList(response: RedmineAgileSprintsResponse): string;
6
+ export declare function formatAgileData(response: RedmineAgileDataResponse): string;
@@ -0,0 +1,53 @@
1
+ export function formatSprint(response) {
2
+ const sprint = response.agile_sprint;
3
+ const lines = [];
4
+ lines.push(`# ${sprint.name}`);
5
+ lines.push("");
6
+ lines.push("| Field | Value |");
7
+ lines.push("|-------|-------|");
8
+ lines.push(`| ID | ${sprint.id} |`);
9
+ lines.push(`| Status | ${sprint.status} |`);
10
+ if (sprint.start_date) {
11
+ lines.push(`| Start Date | ${sprint.start_date} |`);
12
+ }
13
+ if (sprint.end_date) {
14
+ lines.push(`| End Date | ${sprint.end_date} |`);
15
+ }
16
+ if (sprint.description) {
17
+ lines.push(`| Description | ${sprint.description} |`);
18
+ }
19
+ return lines.join("\n");
20
+ }
21
+ export function formatSprintList(response) {
22
+ const sprints = response.agile_sprints;
23
+ if (sprints.length === 0) {
24
+ return "No sprints found.";
25
+ }
26
+ const lines = [];
27
+ lines.push(`# Agile Sprints (${sprints.length})`);
28
+ lines.push("");
29
+ lines.push("| ID | Name | Status | Start | End |");
30
+ lines.push("|----|------|--------|-------|-----|");
31
+ for (const sprint of sprints) {
32
+ const start = sprint.start_date || "-";
33
+ const end = sprint.end_date || "-";
34
+ lines.push(`| ${sprint.id} | ${sprint.name} | ${sprint.status} | ${start} | ${end} |`);
35
+ }
36
+ return lines.join("\n");
37
+ }
38
+ export function formatAgileData(response) {
39
+ const data = response.agile_data;
40
+ const lines = [];
41
+ lines.push(`# Agile Data for Issue #${data.issue_id}`);
42
+ lines.push("");
43
+ lines.push("| Field | Value |");
44
+ lines.push("|-------|-------|");
45
+ lines.push(`| Position | ${data.position} |`);
46
+ if (data.story_points !== undefined) {
47
+ lines.push(`| Story Points | ${data.story_points} |`);
48
+ }
49
+ if (data.agile_sprint_id !== undefined) {
50
+ lines.push(`| Sprint ID | ${data.agile_sprint_id} |`);
51
+ }
52
+ return lines.join("\n");
53
+ }
@@ -0,0 +1,5 @@
1
+ import type { RedmineChecklist, RedmineChecklistsResponse } from "../redmine/types.js";
2
+ export declare function formatChecklist(response: {
3
+ checklist: RedmineChecklist;
4
+ }): string;
5
+ export declare function formatChecklistList(response: RedmineChecklistsResponse): string;
@@ -0,0 +1,30 @@
1
+ export function formatChecklist(response) {
2
+ const item = response.checklist;
3
+ const lines = [];
4
+ lines.push(`# Checklist Item #${item.id}`);
5
+ lines.push("");
6
+ lines.push("| Field | Value |");
7
+ lines.push("|-------|-------|");
8
+ lines.push(`| Subject | ${item.subject} |`);
9
+ lines.push(`| Status | ${item.is_done ? "Done" : "Pending"} |`);
10
+ lines.push(`| Issue | #${item.issue_id} |`);
11
+ lines.push(`| Position | ${item.position} |`);
12
+ return lines.join("\n");
13
+ }
14
+ export function formatChecklistList(response) {
15
+ const items = response.checklists;
16
+ if (items.length === 0) {
17
+ return "No checklist items found.";
18
+ }
19
+ const sorted = [...items].sort((a, b) => a.position - b.position);
20
+ const doneCount = items.filter((i) => i.is_done).length;
21
+ const lines = [];
22
+ lines.push(`# Checklist (${items.length} items)`);
23
+ lines.push(`_${doneCount}/${items.length} completed_`);
24
+ lines.push("");
25
+ for (const item of sorted) {
26
+ const checkbox = item.is_done ? "[x]" : "[ ]";
27
+ lines.push(`- ${checkbox} ${item.subject}`);
28
+ }
29
+ return lines.join("\n");
30
+ }
@@ -0,0 +1,5 @@
1
+ import type { RedmineAttachment, RedmineFilesResponse } from "../redmine/types.js";
2
+ export declare function formatAttachment(response: {
3
+ attachment: RedmineAttachment;
4
+ }): string;
5
+ export declare function formatFileList(response: RedmineFilesResponse): string;
@@ -0,0 +1,61 @@
1
+ import { formatDate, formatDateShort } from "./utils.js";
2
+ function formatFileSize(bytes) {
3
+ if (bytes < 1024)
4
+ return `${bytes} B`;
5
+ if (bytes < 1024 * 1024)
6
+ return `${Math.round(bytes / 1024)} KB`;
7
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
8
+ }
9
+ export function formatAttachment(response) {
10
+ const att = response.attachment;
11
+ const lines = [];
12
+ lines.push(`# ${att.filename}`);
13
+ lines.push("");
14
+ lines.push("| Field | Value |");
15
+ lines.push("|-------|-------|");
16
+ lines.push(`| ID | ${att.id} |`);
17
+ lines.push(`| Size | ${formatFileSize(att.filesize)} |`);
18
+ lines.push(`| Type | ${att.content_type} |`);
19
+ lines.push(`| Author | ${att.author.name} |`);
20
+ lines.push(`| Created | ${formatDate(att.created_on)} |`);
21
+ if (att.description) {
22
+ lines.push(`| Description | ${att.description} |`);
23
+ }
24
+ lines.push("");
25
+ lines.push(`**Download:** ${att.content_url}`);
26
+ if (att.thumbnail_url) {
27
+ lines.push("");
28
+ lines.push(`**Thumbnail:** ${att.thumbnail_url}`);
29
+ }
30
+ return lines.join("\n");
31
+ }
32
+ export function formatFileList(response) {
33
+ const files = response.files;
34
+ if (files.length === 0) {
35
+ return "No files found.";
36
+ }
37
+ const lines = [];
38
+ const hasVersion = files.some((f) => f.version !== undefined);
39
+ lines.push(`# Project Files (${files.length})`);
40
+ lines.push("");
41
+ if (hasVersion) {
42
+ lines.push("| Filename | Size | Version | Downloads | Author | Date |");
43
+ lines.push("|----------|------|---------|-----------|--------|------|");
44
+ }
45
+ else {
46
+ lines.push("| Filename | Size | Downloads | Author | Date |");
47
+ lines.push("|----------|------|-----------|--------|------|");
48
+ }
49
+ for (const file of files) {
50
+ const size = formatFileSize(file.filesize);
51
+ const date = formatDateShort(file.created_on);
52
+ if (hasVersion) {
53
+ const version = file.version?.name || "";
54
+ lines.push(`| ${file.filename} | ${size} | ${version} | ${file.downloads} | ${file.author.name} | ${date} |`);
55
+ }
56
+ else {
57
+ lines.push(`| ${file.filename} | ${size} | ${file.downloads} | ${file.author.name} | ${date} |`);
58
+ }
59
+ }
60
+ return lines.join("\n");
61
+ }
@@ -7,4 +7,10 @@ export { formatWikiPage, formatWikiPageList } from "./wiki.js";
7
7
  export { formatVersion, formatVersionList } from "./version.js";
8
8
  export { formatTimeEntry, formatTimeEntryList } from "./time.js";
9
9
  export { formatGroup, formatGroupList } from "./group.js";
10
- export { formatTrackerList, formatStatusList, formatCategoryList, formatPriorityList, formatActivityList, formatRoleList, formatRole, } from "./metadata.js";
10
+ export { formatTrackerList, formatStatusList, formatCategoryList, formatPriorityList, formatActivityList, formatRoleList, formatRole, formatCategory, formatCustomFieldList, formatQueryList, formatDocumentCategoryList, } from "./metadata.js";
11
+ export { formatRelation, formatRelationList } from "./relation.js";
12
+ export { formatAttachment, formatFileList } from "./file.js";
13
+ export { formatMembership, formatMembershipList } from "./membership.js";
14
+ export { formatChecklist, formatChecklistList } from "./checklist.js";
15
+ export { formatSprint, formatSprintList, formatAgileData } from "./agile.js";
16
+ export { formatMyAccount } from "./account.js";
@@ -7,4 +7,10 @@ export { formatWikiPage, formatWikiPageList } from "./wiki.js";
7
7
  export { formatVersion, formatVersionList } from "./version.js";
8
8
  export { formatTimeEntry, formatTimeEntryList } from "./time.js";
9
9
  export { formatGroup, formatGroupList } from "./group.js";
10
- export { formatTrackerList, formatStatusList, formatCategoryList, formatPriorityList, formatActivityList, formatRoleList, formatRole, } from "./metadata.js";
10
+ export { formatTrackerList, formatStatusList, formatCategoryList, formatPriorityList, formatActivityList, formatRoleList, formatRole, formatCategory, formatCustomFieldList, formatQueryList, formatDocumentCategoryList, } from "./metadata.js";
11
+ export { formatRelation, formatRelationList } from "./relation.js";
12
+ export { formatAttachment, formatFileList } from "./file.js";
13
+ export { formatMembership, formatMembershipList } from "./membership.js";
14
+ export { formatChecklist, formatChecklistList } from "./checklist.js";
15
+ export { formatSprint, formatSprintList, formatAgileData } from "./agile.js";
16
+ export { formatMyAccount } from "./account.js";
@@ -1,15 +1,21 @@
1
1
  import type { RedmineIssue, RedmineIssuesResponse } from "../redmine/types.js";
2
2
  import { type NameLookup } from "./journals.js";
3
+ /**
4
+ * Options for formatting issues
5
+ */
6
+ export interface IssueFormatOptions {
7
+ includeDescriptionDiffs?: boolean;
8
+ }
3
9
  /**
4
10
  * Format an issue as complete Markdown
5
11
  */
6
- export declare function formatIssue(issue: RedmineIssue, lookup?: NameLookup): string;
12
+ export declare function formatIssue(issue: RedmineIssue, lookup?: NameLookup, options?: IssueFormatOptions): string;
7
13
  /**
8
14
  * Format an issue API response as Markdown
9
15
  */
10
16
  export declare function formatIssueResponse(response: {
11
17
  issue: RedmineIssue;
12
- }, lookup?: NameLookup): string;
18
+ }, lookup?: NameLookup, options?: IssueFormatOptions): string;
13
19
  /**
14
20
  * Format a list of issues as a Markdown table
15
21
  */
@@ -1,14 +1,9 @@
1
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
- }
2
+ import { formatDate, formatDateShort } from "./utils.js";
8
3
  /**
9
4
  * Format an issue as complete Markdown
10
5
  */
11
- export function formatIssue(issue, lookup = {}) {
6
+ export function formatIssue(issue, lookup = {}, options = {}) {
12
7
  const lines = [];
13
8
  // Title
14
9
  lines.push(`# #${issue.id}: ${issue.subject}`);
@@ -116,21 +111,17 @@ export function formatIssue(issue, lookup = {}) {
116
111
  }
117
112
  // Journals (history)
118
113
  if (issue.journals && issue.journals.length > 0) {
119
- lines.push(formatJournals(issue.journals, lookup));
114
+ lines.push(formatJournals(issue.journals, lookup, {
115
+ includeDescriptionDiffs: options.includeDescriptionDiffs,
116
+ }));
120
117
  }
121
118
  return lines.join("\n");
122
119
  }
123
120
  /**
124
121
  * Format an issue API response as Markdown
125
122
  */
126
- export function formatIssueResponse(response, lookup = {}) {
127
- return formatIssue(response.issue, lookup);
128
- }
129
- /**
130
- * Format a date string to YYYY-MM-DD format
131
- */
132
- function formatDateShort(isoDate) {
133
- return new Date(isoDate).toISOString().slice(0, 10);
123
+ export function formatIssueResponse(response, lookup = {}, options = {}) {
124
+ return formatIssue(response.issue, lookup, options);
134
125
  }
135
126
  /**
136
127
  * Format a list of issues as a Markdown table
@@ -4,7 +4,15 @@ import type { RedmineJournal } from "../redmine/types.js";
4
4
  * Key: field name (e.g., "status_id"), Value: map of ID -> name
5
5
  */
6
6
  export type NameLookup = Record<string, Record<string, string>>;
7
+ /**
8
+ * Options for formatting journals
9
+ */
10
+ export interface JournalFormatOptions {
11
+ includeDescriptionDiffs?: boolean;
12
+ }
7
13
  /**
8
14
  * Format an array of journal entries as Markdown
15
+ * Entries are displayed in reverse chronological order (newest first)
16
+ * Note numbers are preserved (1 = oldest, highest = newest)
9
17
  */
10
- export declare function formatJournals(journals: RedmineJournal[], lookup?: NameLookup): string;
18
+ export declare function formatJournals(journals: RedmineJournal[], lookup?: NameLookup, options?: JournalFormatOptions): string;
@@ -1,4 +1,5 @@
1
1
  import { diffLines } from "diff";
2
+ import { formatDate } from "./utils.js";
2
3
  /**
3
4
  * Fields that contain large text and should use diff formatting
4
5
  */
@@ -68,13 +69,6 @@ const FIELD_DISPLAY_NAMES = {
68
69
  parent_id: "parent",
69
70
  project_id: "project",
70
71
  };
71
- /**
72
- * Format a date string to a readable format
73
- */
74
- function formatDate(isoDate) {
75
- const date = new Date(isoDate);
76
- return date.toISOString().slice(0, 16).replace("T", " ");
77
- }
78
72
  /**
79
73
  * Generate a unified diff for large text changes
80
74
  */
@@ -116,7 +110,7 @@ function resolveValue(fieldName, value, lookup) {
116
110
  /**
117
111
  * Format a single journal detail (field change)
118
112
  */
119
- function formatDetail(detail, lookup) {
113
+ function formatDetail(detail, lookup, options = {}) {
120
114
  const { property, name, old_value, new_value } = detail;
121
115
  // Handle attachment additions/removals
122
116
  if (property === "attachment") {
@@ -157,8 +151,11 @@ function formatDetail(detail, lookup) {
157
151
  }
158
152
  // Fall through to default formatting if parsing failed
159
153
  }
160
- // Large text fields get diff formatting
154
+ // Large text fields get diff formatting (only if option enabled)
161
155
  if (LARGE_TEXT_FIELDS.includes(name) && old_value && new_value) {
156
+ if (!options.includeDescriptionDiffs) {
157
+ return `- ${name}: _(changed - use include_description_diffs to see diff)_`;
158
+ }
162
159
  const diff = generateDiff(old_value, new_value);
163
160
  return `- ${name}:\n\`\`\`diff\n${diff}\n\`\`\``;
164
161
  }
@@ -181,13 +178,14 @@ function formatDetail(detail, lookup) {
181
178
  /**
182
179
  * Format a single journal entry as Markdown
183
180
  */
184
- function formatJournalEntry(journal, lookup) {
181
+ function formatJournalEntry(journal, index, lookup, options = {}) {
185
182
  const lines = [];
186
- // Header with date and user
183
+ // Header with note number, date and user
184
+ const noteNum = index + 1;
187
185
  const date = formatDate(journal.created_on);
188
186
  const user = journal.user.name;
189
187
  const privateTag = journal.private_notes ? " 🔒" : "";
190
- lines.push(`### ${date} - ${user}${privateTag}`);
188
+ lines.push(`### #${noteNum} - ${date} - ${user}${privateTag}`);
191
189
  lines.push("");
192
190
  // Notes (if any)
193
191
  if (journal.notes && journal.notes.trim()) {
@@ -198,7 +196,7 @@ function formatJournalEntry(journal, lookup) {
198
196
  if (journal.details && journal.details.length > 0) {
199
197
  lines.push("**Changes:**");
200
198
  for (const detail of journal.details) {
201
- lines.push(formatDetail(detail, lookup));
199
+ lines.push(formatDetail(detail, lookup, options));
202
200
  }
203
201
  lines.push("");
204
202
  }
@@ -206,12 +204,16 @@ function formatJournalEntry(journal, lookup) {
206
204
  }
207
205
  /**
208
206
  * Format an array of journal entries as Markdown
207
+ * Entries are displayed in reverse chronological order (newest first)
208
+ * Note numbers are preserved (1 = oldest, highest = newest)
209
209
  */
210
- export function formatJournals(journals, lookup = {}) {
210
+ export function formatJournals(journals, lookup = {}, options = {}) {
211
211
  if (!journals || journals.length === 0) {
212
212
  return "";
213
213
  }
214
214
  const header = `## History (${journals.length} entries)\n\n`;
215
- const entries = journals.map(j => formatJournalEntry(j, lookup)).join("\n---\n\n");
215
+ // Format entries with their original indices (for note numbers), then reverse for display
216
+ const formattedEntries = journals.map((j, i) => formatJournalEntry(j, i, lookup, options));
217
+ const entries = formattedEntries.reverse().join("\n---\n\n");
216
218
  return header + entries;
217
219
  }
@@ -0,0 +1,5 @@
1
+ import type { RedmineMembership, RedmineMembershipsResponse } from "../redmine/types.js";
2
+ export declare function formatMembership(response: {
3
+ membership: RedmineMembership;
4
+ }): string;
5
+ export declare function formatMembershipList(response: RedmineMembershipsResponse): string;
@@ -0,0 +1,41 @@
1
+ function formatRoles(roles) {
2
+ return roles
3
+ .map((r) => (r.inherited ? `${r.name} (inherited)` : r.name))
4
+ .join(", ");
5
+ }
6
+ export function formatMembership(response) {
7
+ const mem = response.membership;
8
+ const lines = [];
9
+ lines.push(`# Membership #${mem.id}`);
10
+ lines.push("");
11
+ lines.push("| Field | Value |");
12
+ lines.push("|-------|-------|");
13
+ lines.push(`| Project | ${mem.project.name} |`);
14
+ if (mem.user) {
15
+ lines.push(`| User | ${mem.user.name} |`);
16
+ }
17
+ if (mem.group) {
18
+ lines.push(`| Group | ${mem.group.name} |`);
19
+ }
20
+ lines.push(`| Roles | ${formatRoles(mem.roles)} |`);
21
+ return lines.join("\n");
22
+ }
23
+ export function formatMembershipList(response) {
24
+ const memberships = response.memberships;
25
+ const totalCount = response.total_count;
26
+ if (memberships.length === 0) {
27
+ return "No memberships found.";
28
+ }
29
+ const lines = [];
30
+ lines.push(`# Project Memberships (${totalCount})`);
31
+ lines.push("");
32
+ lines.push("| ID | User/Group | Type | Roles |");
33
+ lines.push("|----|------------|------|-------|");
34
+ for (const mem of memberships) {
35
+ const name = mem.user?.name || mem.group?.name || "Unknown";
36
+ const type = mem.user ? "User" : "Group";
37
+ const roles = formatRoles(mem.roles);
38
+ lines.push(`| ${mem.id} | ${name} | ${type} | ${roles} |`);
39
+ }
40
+ return lines.join("\n");
41
+ }
@@ -55,6 +55,35 @@ interface RedmineRole {
55
55
  interface RedmineRolesResponse {
56
56
  roles: RedmineRole[];
57
57
  }
58
+ interface RedmineCustomField {
59
+ id: number;
60
+ name: string;
61
+ customized_type: string;
62
+ field_format: string;
63
+ is_required: boolean;
64
+ is_filter: boolean;
65
+ searchable: boolean;
66
+ }
67
+ interface RedmineCustomFieldsResponse {
68
+ custom_fields: RedmineCustomField[];
69
+ }
70
+ interface RedmineQuery {
71
+ id: number;
72
+ name: string;
73
+ is_public: boolean;
74
+ project_id?: number;
75
+ }
76
+ interface RedmineQueriesResponse {
77
+ queries: RedmineQuery[];
78
+ }
79
+ interface RedmineDocumentCategory {
80
+ id: number;
81
+ name: string;
82
+ is_default: boolean;
83
+ }
84
+ interface RedmineDocumentCategoriesResponse {
85
+ document_categories: RedmineDocumentCategory[];
86
+ }
58
87
  /**
59
88
  * Format a list of trackers as a Markdown table
60
89
  */
@@ -85,4 +114,22 @@ export declare function formatRoleList(response: RedmineRolesResponse): string;
85
114
  export declare function formatRole(response: {
86
115
  role: RedmineRole;
87
116
  }): string;
117
+ /**
118
+ * Format a single issue category as Markdown
119
+ */
120
+ export declare function formatCategory(response: {
121
+ issue_category: RedmineCategory;
122
+ }): string;
123
+ /**
124
+ * Format a list of custom fields as a Markdown table
125
+ */
126
+ export declare function formatCustomFieldList(response: RedmineCustomFieldsResponse): string;
127
+ /**
128
+ * Format a list of saved queries as a Markdown table
129
+ */
130
+ export declare function formatQueryList(response: RedmineQueriesResponse): string;
131
+ /**
132
+ * Format a list of document categories as a Markdown table
133
+ */
134
+ export declare function formatDocumentCategoryList(response: RedmineDocumentCategoriesResponse): string;
88
135
  export {};
@@ -135,3 +135,79 @@ export function formatRole(response) {
135
135
  }
136
136
  return lines.join("\n").trimEnd();
137
137
  }
138
+ /**
139
+ * Format a single issue category as Markdown
140
+ */
141
+ export function formatCategory(response) {
142
+ const cat = response.issue_category;
143
+ const lines = [];
144
+ lines.push(`# ${cat.name}`);
145
+ lines.push("");
146
+ lines.push("| Field | Value |");
147
+ lines.push("|-------|-------|");
148
+ lines.push(`| ID | ${cat.id} |`);
149
+ if (cat.project) {
150
+ lines.push(`| Project | ${cat.project.name} |`);
151
+ }
152
+ if (cat.assigned_to) {
153
+ lines.push(`| Default Assignee | ${cat.assigned_to.name} |`);
154
+ }
155
+ return lines.join("\n");
156
+ }
157
+ /**
158
+ * Format a list of custom fields as a Markdown table
159
+ */
160
+ export function formatCustomFieldList(response) {
161
+ const fields = response.custom_fields;
162
+ if (fields.length === 0) {
163
+ return "No custom fields found.";
164
+ }
165
+ const lines = [];
166
+ lines.push(`# Custom Fields (${fields.length})`);
167
+ lines.push("");
168
+ lines.push("| ID | Name | Type | Format | Required |");
169
+ lines.push("|----|------|------|--------|----------|");
170
+ for (const field of fields) {
171
+ const required = field.is_required ? "Yes" : "No";
172
+ lines.push(`| ${field.id} | ${field.name} | ${field.customized_type} | ${field.field_format} | ${required} |`);
173
+ }
174
+ return lines.join("\n");
175
+ }
176
+ /**
177
+ * Format a list of saved queries as a Markdown table
178
+ */
179
+ export function formatQueryList(response) {
180
+ const queries = response.queries;
181
+ if (queries.length === 0) {
182
+ return "No queries found.";
183
+ }
184
+ const lines = [];
185
+ lines.push(`# Saved Queries (${queries.length})`);
186
+ lines.push("");
187
+ lines.push("| ID | Name | Visibility |");
188
+ lines.push("|----|------|------------|");
189
+ for (const query of queries) {
190
+ const visibility = query.is_public ? "Public" : "Private";
191
+ lines.push(`| ${query.id} | ${query.name} | ${visibility} |`);
192
+ }
193
+ return lines.join("\n");
194
+ }
195
+ /**
196
+ * Format a list of document categories as a Markdown table
197
+ */
198
+ export function formatDocumentCategoryList(response) {
199
+ const categories = response.document_categories;
200
+ if (categories.length === 0) {
201
+ return "No document categories found.";
202
+ }
203
+ const lines = [];
204
+ lines.push(`# Document Categories (${categories.length})`);
205
+ lines.push("");
206
+ lines.push("| ID | Name | Default |");
207
+ lines.push("|----|------|---------|");
208
+ for (const cat of categories) {
209
+ const isDefault = cat.is_default ? "Yes" : "No";
210
+ lines.push(`| ${cat.id} | ${cat.name} | ${isDefault} |`);
211
+ }
212
+ return lines.join("\n");
213
+ }
@@ -1,14 +1,9 @@
1
+ import { formatDate } from "./utils.js";
1
2
  const PROJECT_STATUS = {
2
3
  1: "Active",
3
4
  5: "Closed",
4
5
  9: "Archived",
5
6
  };
6
- /**
7
- * Format a date string to readable format
8
- */
9
- function formatDate(isoDate) {
10
- return new Date(isoDate).toISOString().slice(0, 16).replace("T", " ");
11
- }
12
7
  /**
13
8
  * Format a single project as complete Markdown
14
9
  */
@@ -0,0 +1,5 @@
1
+ import type { RedmineRelation, RedmineRelationsResponse } from "../redmine/types.js";
2
+ export declare function formatRelation(response: {
3
+ relation: RedmineRelation;
4
+ }): string;
5
+ export declare function formatRelationList(response: RedmineRelationsResponse): string;
@@ -0,0 +1,55 @@
1
+ const RELATION_LABELS = {
2
+ relates: "relates",
3
+ duplicates: "duplicates",
4
+ duplicated: "duplicated by",
5
+ blocks: "blocks",
6
+ blocked: "blocked by",
7
+ precedes: "precedes",
8
+ follows: "follows",
9
+ copied_to: "copied to",
10
+ copied_from: "copied from",
11
+ };
12
+ export function formatRelation(response) {
13
+ const rel = response.relation;
14
+ const lines = [];
15
+ lines.push(`# Relation #${rel.id}`);
16
+ lines.push("");
17
+ lines.push("| Field | Value |");
18
+ lines.push("|-------|-------|");
19
+ lines.push(`| Issue | #${rel.issue_id} |`);
20
+ lines.push(`| Type | ${RELATION_LABELS[rel.relation_type] || rel.relation_type} |`);
21
+ lines.push(`| Related To | #${rel.issue_to_id} |`);
22
+ if (rel.delay !== undefined && rel.delay > 0) {
23
+ lines.push(`| Delay | ${rel.delay} days |`);
24
+ }
25
+ return lines.join("\n");
26
+ }
27
+ export function formatRelationList(response) {
28
+ const relations = response.relations;
29
+ if (relations.length === 0) {
30
+ return "No relations found.";
31
+ }
32
+ const lines = [];
33
+ const hasDelay = relations.some((r) => r.delay !== undefined && r.delay > 0);
34
+ lines.push(`# Issue Relations (${relations.length})`);
35
+ lines.push("");
36
+ if (hasDelay) {
37
+ lines.push("| ID | Issue | Type | Related To | Delay |");
38
+ lines.push("|----|-------|------|------------|-------|");
39
+ }
40
+ else {
41
+ lines.push("| ID | Issue | Type | Related To |");
42
+ lines.push("|----|-------|------|------------|");
43
+ }
44
+ for (const rel of relations) {
45
+ const type = RELATION_LABELS[rel.relation_type] || rel.relation_type;
46
+ if (hasDelay) {
47
+ const delay = rel.delay ? `${rel.delay} days` : "";
48
+ lines.push(`| ${rel.id} | #${rel.issue_id} | ${type} | #${rel.issue_to_id} | ${delay} |`);
49
+ }
50
+ else {
51
+ lines.push(`| ${rel.id} | #${rel.issue_id} | ${type} | #${rel.issue_to_id} |`);
52
+ }
53
+ }
54
+ return lines.join("\n");
55
+ }
@@ -1,3 +1,4 @@
1
+ import { formatDateShort, truncate } from "./utils.js";
1
2
  /**
2
3
  * Get icon for search result type
3
4
  */
@@ -13,21 +14,6 @@ function getTypeIcon(type) {
13
14
  return "\u{1F4CB}"; // clipboard
14
15
  }
15
16
  }
16
- /**
17
- * Format a date string to YYYY-MM-DD format
18
- */
19
- function formatDateShort(isoDate) {
20
- return new Date(isoDate).toISOString().slice(0, 10);
21
- }
22
- /**
23
- * Truncate text to a maximum length with ellipsis
24
- */
25
- function truncate(text, maxLength) {
26
- if (text.length <= maxLength) {
27
- return text;
28
- }
29
- return text.slice(0, maxLength) + "...";
30
- }
31
17
  /**
32
18
  * Format a single search result as Markdown
33
19
  */
@@ -1,14 +1,9 @@
1
+ import { formatDate } from "./utils.js";
1
2
  const USER_STATUS = {
2
3
  1: "Active",
3
4
  2: "Registered",
4
5
  3: "Locked",
5
6
  };
6
- /**
7
- * Format a date string to readable format
8
- */
9
- function formatDate(isoDate) {
10
- return new Date(isoDate).toISOString().slice(0, 16).replace("T", " ");
11
- }
12
7
  /**
13
8
  * Format a single user as complete Markdown
14
9
  */
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Format an ISO date string to readable local datetime format (YYYY-MM-DD HH:mm)
3
+ */
4
+ export declare function formatDate(isoDate: string): string;
5
+ /**
6
+ * Format an ISO date string to short date format (YYYY-MM-DD)
7
+ */
8
+ export declare function formatDateShort(isoDate: string): string;
9
+ /**
10
+ * Truncate text to a maximum length with ellipsis
11
+ */
12
+ export declare function truncate(text: string, maxLength: number): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Format an ISO date string to readable local datetime format (YYYY-MM-DD HH:mm)
3
+ */
4
+ export function formatDate(isoDate) {
5
+ const date = new Date(isoDate);
6
+ const year = date.getFullYear();
7
+ const month = String(date.getMonth() + 1).padStart(2, "0");
8
+ const day = String(date.getDate()).padStart(2, "0");
9
+ const hours = String(date.getHours()).padStart(2, "0");
10
+ const minutes = String(date.getMinutes()).padStart(2, "0");
11
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
12
+ }
13
+ /**
14
+ * Format an ISO date string to short date format (YYYY-MM-DD)
15
+ */
16
+ export function formatDateShort(isoDate) {
17
+ const date = new Date(isoDate);
18
+ const year = date.getFullYear();
19
+ const month = String(date.getMonth() + 1).padStart(2, "0");
20
+ const day = String(date.getDate()).padStart(2, "0");
21
+ return `${year}-${month}-${day}`;
22
+ }
23
+ /**
24
+ * Truncate text to a maximum length with ellipsis
25
+ */
26
+ export function truncate(text, maxLength) {
27
+ if (text.length <= maxLength) {
28
+ return text;
29
+ }
30
+ return text.slice(0, maxLength) + "...";
31
+ }
@@ -1,9 +1,4 @@
1
- /**
2
- * Format a date string to readable format (YYYY-MM-DD)
3
- */
4
- function formatDate(isoDate) {
5
- return new Date(isoDate).toISOString().slice(0, 10);
6
- }
1
+ import { formatDate } from "./utils.js";
7
2
  /**
8
3
  * Format a single wiki page as Markdown
9
4
  */
@@ -3,6 +3,10 @@ export declare class RedmineClient {
3
3
  private baseUrl;
4
4
  private apiKey;
5
5
  constructor(baseUrl: string, apiKey: string);
6
+ /**
7
+ * Get the base URL of the Redmine instance
8
+ */
9
+ getBaseUrl(): string;
6
10
  private request;
7
11
  listIssues(params?: {
8
12
  project_id?: string | number;
@@ -7,6 +7,12 @@ export class RedmineClient {
7
7
  // Remove trailing slash if present
8
8
  this.baseUrl = baseUrl.replace(/\/$/, "");
9
9
  }
10
+ /**
11
+ * Get the base URL of the Redmine instance
12
+ */
13
+ getBaseUrl() {
14
+ return this.baseUrl;
15
+ }
10
16
  async request(method, path, body) {
11
17
  try {
12
18
  const response = await fetch(`${this.baseUrl}${path}`, {
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.5.0",
6
+ version: "0.5.2",
7
7
  });
8
8
  registerTools(server, redmineClient, toolGroups);
9
9
  return server;
@@ -1,10 +1,16 @@
1
+ import { formatMyAccount } from "../formatters/index.js";
1
2
  export function registerAccountTools(server, client) {
2
3
  server.registerTool("get_my_account", {
3
4
  description: "Get current user account information",
4
5
  }, async () => {
5
6
  const result = await client.getMyAccount();
7
+ if ("error" in result) {
8
+ return {
9
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
10
+ };
11
+ }
6
12
  return {
7
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
13
+ content: [{ type: "text", text: formatMyAccount(result) }],
8
14
  };
9
15
  });
10
16
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { formatSprint, formatSprintList, formatAgileData } from "../formatters/index.js";
2
3
  export function registerAgileTools(server, client) {
3
4
  // === SPRINTS ===
4
5
  server.registerTool("list_agile_sprints", {
@@ -8,8 +9,13 @@ export function registerAgileTools(server, client) {
8
9
  },
9
10
  }, async (params) => {
10
11
  const result = await client.listAgileSprints(params.project_id);
12
+ if ("error" in result) {
13
+ return {
14
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
15
+ };
16
+ }
11
17
  return {
12
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
18
+ content: [{ type: "text", text: formatSprintList(result) }],
13
19
  };
14
20
  });
15
21
  server.registerTool("get_agile_sprint", {
@@ -20,8 +26,13 @@ export function registerAgileTools(server, client) {
20
26
  },
21
27
  }, async (params) => {
22
28
  const result = await client.getAgileSprint(params.project_id, params.sprint_id);
29
+ if ("error" in result) {
30
+ return {
31
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
32
+ };
33
+ }
23
34
  return {
24
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
35
+ content: [{ type: "text", text: formatSprint(result) }],
25
36
  };
26
37
  });
27
38
  server.registerTool("create_agile_sprint", {
@@ -81,8 +92,13 @@ export function registerAgileTools(server, client) {
81
92
  },
82
93
  }, async (params) => {
83
94
  const result = await client.getIssueAgileData(params.issue_id);
95
+ if ("error" in result) {
96
+ return {
97
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
98
+ };
99
+ }
84
100
  return {
85
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
101
+ content: [{ type: "text", text: formatAgileData(result) }],
86
102
  };
87
103
  });
88
104
  server.registerTool("update_issue_agile_data", {
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { formatChecklist, formatChecklistList } from "../formatters/index.js";
2
3
  export function registerChecklistsTools(server, client) {
3
4
  server.registerTool("list_checklist_items", {
4
5
  description: "List all checklist items for an issue (requires redmine_checklists plugin)",
@@ -7,8 +8,13 @@ export function registerChecklistsTools(server, client) {
7
8
  },
8
9
  }, async (params) => {
9
10
  const result = await client.listChecklists(params.issue_id);
11
+ if ("error" in result) {
12
+ return {
13
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
14
+ };
15
+ }
10
16
  return {
11
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
17
+ content: [{ type: "text", text: formatChecklistList(result) }],
12
18
  };
13
19
  });
14
20
  server.registerTool("get_checklist_item", {
@@ -18,8 +24,13 @@ export function registerChecklistsTools(server, client) {
18
24
  },
19
25
  }, async (params) => {
20
26
  const result = await client.getChecklist(params.checklist_id);
27
+ if ("error" in result) {
28
+ return {
29
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
30
+ };
31
+ }
21
32
  return {
22
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
33
+ content: [{ type: "text", text: formatChecklist(result) }],
23
34
  };
24
35
  });
25
36
  server.registerTool("create_checklist_item", {
@@ -38,6 +38,7 @@ export function registerCoreTools(server, client) {
38
38
  inputSchema: {
39
39
  issue_id: z.number().describe("The issue ID"),
40
40
  include: z.string().optional().describe("Include: attachments, relations, journals, watchers, children, changesets, allowed_statuses"),
41
+ include_description_diffs: z.boolean().optional().default(false).describe("Include full description diffs in history (can be verbose)"),
41
42
  },
42
43
  }, async (params) => {
43
44
  // Fetch issue and enumerations in parallel
@@ -76,7 +77,9 @@ export function registerCoreTools(server, client) {
76
77
  }
77
78
  // Format response as Markdown
78
79
  return {
79
- content: [{ type: "text", text: formatIssueResponse(result, lookup) }],
80
+ content: [{ type: "text", text: formatIssueResponse(result, lookup, {
81
+ includeDescriptionDiffs: params.include_description_diffs,
82
+ }) }],
80
83
  };
81
84
  });
82
85
  server.registerTool("create_issue", {
@@ -1,4 +1,4 @@
1
- import { formatPriorityList, formatActivityList } from "../formatters/index.js";
1
+ import { formatPriorityList, formatActivityList, formatDocumentCategoryList } from "../formatters/index.js";
2
2
  export function registerEnumerationsTools(server, client) {
3
3
  server.registerTool("list_issue_priorities", {
4
4
  description: "List all issue priorities with their IDs (Low, Normal, High, Urgent, Immediate)",
@@ -30,8 +30,13 @@ export function registerEnumerationsTools(server, client) {
30
30
  description: "List all document categories with their IDs",
31
31
  }, async () => {
32
32
  const result = await client.listDocumentCategories();
33
+ if ("error" in result) {
34
+ return {
35
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
36
+ };
37
+ }
33
38
  return {
34
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
39
+ content: [{ type: "text", text: formatDocumentCategoryList(result) }],
35
40
  };
36
41
  });
37
42
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { readFile } from "fs/promises";
3
+ import { formatAttachment, formatFileList } from "../formatters/index.js";
3
4
  export function registerFilesTools(server, client) {
4
5
  // === ATTACHMENTS ===
5
6
  server.registerTool("get_attachment", {
@@ -9,8 +10,13 @@ export function registerFilesTools(server, client) {
9
10
  },
10
11
  }, async (params) => {
11
12
  const result = await client.getAttachment(params.attachment_id);
13
+ if ("error" in result) {
14
+ return {
15
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
16
+ };
17
+ }
12
18
  return {
13
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
19
+ content: [{ type: "text", text: formatAttachment(result) }],
14
20
  };
15
21
  });
16
22
  server.registerTool("delete_attachment", {
@@ -47,8 +53,13 @@ export function registerFilesTools(server, client) {
47
53
  },
48
54
  }, async (params) => {
49
55
  const result = await client.listProjectFiles(params.project_id);
56
+ if ("error" in result) {
57
+ return {
58
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
59
+ };
60
+ }
50
61
  return {
51
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
62
+ content: [{ type: "text", text: formatFileList(result) }],
52
63
  };
53
64
  });
54
65
  server.registerTool("upload_project_file", {
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { formatMembership, formatMembershipList } from "../formatters/index.js";
2
3
  export function registerMembershipsTools(server, client) {
3
4
  server.registerTool("list_project_memberships", {
4
5
  description: "List all memberships (users and groups) for a project",
@@ -10,8 +11,13 @@ export function registerMembershipsTools(server, client) {
10
11
  }, async (params) => {
11
12
  const { project_id, ...rest } = params;
12
13
  const result = await client.listProjectMemberships(project_id, rest);
14
+ if ("error" in result) {
15
+ return {
16
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
17
+ };
18
+ }
13
19
  return {
14
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
20
+ content: [{ type: "text", text: formatMembershipList(result) }],
15
21
  };
16
22
  });
17
23
  server.registerTool("get_membership", {
@@ -21,8 +27,13 @@ export function registerMembershipsTools(server, client) {
21
27
  },
22
28
  }, async (params) => {
23
29
  const result = await client.getMembership(params.membership_id);
30
+ if ("error" in result) {
31
+ return {
32
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
33
+ };
34
+ }
24
35
  return {
25
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
36
+ content: [{ type: "text", text: formatMembership(result) }],
26
37
  };
27
38
  });
28
39
  server.registerTool("create_project_membership", {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { formatTrackerList, formatStatusList, formatCategoryList } from "../formatters/index.js";
2
+ import { formatTrackerList, formatStatusList, formatCategoryList, formatCategory, formatCustomFieldList, formatQueryList } from "../formatters/index.js";
3
3
  export function registerMetadataTools(server, client) {
4
4
  // === TRACKERS ===
5
5
  server.registerTool("list_trackers", {
@@ -53,8 +53,13 @@ export function registerMetadataTools(server, client) {
53
53
  },
54
54
  }, async (params) => {
55
55
  const result = await client.getIssueCategory(params.category_id);
56
+ if ("error" in result) {
57
+ return {
58
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
59
+ };
60
+ }
56
61
  return {
57
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
62
+ content: [{ type: "text", text: formatCategory(result) }],
58
63
  };
59
64
  });
60
65
  server.registerTool("create_issue_category", {
@@ -102,8 +107,13 @@ export function registerMetadataTools(server, client) {
102
107
  description: "List all custom field definitions (requires admin privileges)",
103
108
  }, async () => {
104
109
  const result = await client.listCustomFields();
110
+ if ("error" in result) {
111
+ return {
112
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
113
+ };
114
+ }
105
115
  return {
106
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
116
+ content: [{ type: "text", text: formatCustomFieldList(result) }],
107
117
  };
108
118
  });
109
119
  // === QUERIES ===
@@ -111,8 +121,13 @@ export function registerMetadataTools(server, client) {
111
121
  description: "List all saved issue queries (public and private)",
112
122
  }, async () => {
113
123
  const result = await client.listQueries();
124
+ if ("error" in result) {
125
+ return {
126
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
127
+ };
128
+ }
114
129
  return {
115
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
130
+ content: [{ type: "text", text: formatQueryList(result) }],
116
131
  };
117
132
  });
118
133
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { formatVersion, formatVersionList } from "../formatters/index.js";
2
+ import { formatVersion, formatVersionList, formatRelation, formatRelationList } from "../formatters/index.js";
3
3
  export function registerRelationsTools(server, client) {
4
4
  // === ISSUE RELATIONS ===
5
5
  server.registerTool("list_issue_relations", {
@@ -9,8 +9,13 @@ export function registerRelationsTools(server, client) {
9
9
  },
10
10
  }, async (params) => {
11
11
  const result = await client.listIssueRelations(params.issue_id);
12
+ if ("error" in result) {
13
+ return {
14
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
15
+ };
16
+ }
12
17
  return {
13
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
18
+ content: [{ type: "text", text: formatRelationList(result) }],
14
19
  };
15
20
  });
16
21
  server.registerTool("get_relation", {
@@ -20,8 +25,13 @@ export function registerRelationsTools(server, client) {
20
25
  },
21
26
  }, async (params) => {
22
27
  const result = await client.getRelation(params.relation_id);
28
+ if ("error" in result) {
29
+ return {
30
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
31
+ };
32
+ }
23
33
  return {
24
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
34
+ content: [{ type: "text", text: formatRelation(result) }],
25
35
  };
26
36
  });
27
37
  server.registerTool("create_issue_relation", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pschroee/redmine-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "MCP server for Redmine - full API access with configurable tool groups",
5
5
  "type": "module",
6
6
  "bin": {