@pschroee/redmine-mcp 0.4.5 → 0.5.0
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/group.d.ts +11 -0
- package/dist/formatters/group.js +53 -0
- package/dist/formatters/index.d.ts +9 -1
- package/dist/formatters/index.js +9 -1
- package/dist/formatters/issue.d.ts +5 -1
- package/dist/formatters/issue.js +42 -0
- package/dist/formatters/metadata.d.ts +88 -0
- package/dist/formatters/metadata.js +137 -0
- package/dist/formatters/project.d.ts +11 -0
- package/dist/formatters/project.js +87 -0
- package/dist/formatters/search.d.ts +5 -0
- package/dist/formatters/search.js +75 -0
- package/dist/formatters/time.d.ts +11 -0
- package/dist/formatters/time.js +47 -0
- package/dist/formatters/user.d.ts +11 -0
- package/dist/formatters/user.js +88 -0
- package/dist/formatters/version.d.ts +11 -0
- package/dist/formatters/version.js +75 -0
- package/dist/formatters/wiki.d.ts +11 -0
- package/dist/formatters/wiki.js +91 -0
- package/dist/server.js +1 -1
- package/dist/tools/admin.js +25 -4
- package/dist/tools/core.js +19 -4
- package/dist/tools/enumerations.js +13 -2
- package/dist/tools/metadata.js +19 -3
- package/dist/tools/relations.js +13 -2
- package/dist/tools/roles.js +13 -2
- package/dist/tools/search.js +7 -1
- package/dist/tools/time.js +13 -2
- package/dist/tools/wiki.js +13 -2
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineGroup, RedmineGroupsResponse } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a single group as complete Markdown
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatGroup(response: {
|
|
6
|
+
group: RedmineGroup;
|
|
7
|
+
}): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format a list of groups as Markdown table
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatGroupList(response: RedmineGroupsResponse): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a single group as complete Markdown
|
|
3
|
+
*/
|
|
4
|
+
export function formatGroup(response) {
|
|
5
|
+
const group = response.group;
|
|
6
|
+
const lines = [];
|
|
7
|
+
// Header
|
|
8
|
+
lines.push(`# ${group.name}`);
|
|
9
|
+
lines.push("");
|
|
10
|
+
// Users
|
|
11
|
+
if (group.users && group.users.length > 0) {
|
|
12
|
+
lines.push("## Users");
|
|
13
|
+
lines.push("");
|
|
14
|
+
for (const user of group.users) {
|
|
15
|
+
lines.push(`- ${user.name}`);
|
|
16
|
+
}
|
|
17
|
+
lines.push("");
|
|
18
|
+
}
|
|
19
|
+
// Project Memberships
|
|
20
|
+
if (group.memberships && group.memberships.length > 0) {
|
|
21
|
+
lines.push("## Project Memberships");
|
|
22
|
+
lines.push("");
|
|
23
|
+
for (const membership of group.memberships) {
|
|
24
|
+
const roles = membership.roles.map((r) => r.name).join(", ");
|
|
25
|
+
lines.push(`- **${membership.project.name}:** ${roles}`);
|
|
26
|
+
}
|
|
27
|
+
lines.push("");
|
|
28
|
+
}
|
|
29
|
+
return lines.join("\n").trimEnd();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Format a list of groups as Markdown table
|
|
33
|
+
*/
|
|
34
|
+
export function formatGroupList(response) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
// Header
|
|
37
|
+
lines.push(`# Groups (${response.total_count})`);
|
|
38
|
+
lines.push("");
|
|
39
|
+
// Empty case
|
|
40
|
+
if (response.groups.length === 0) {
|
|
41
|
+
lines.push("No groups found.");
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
// Table header
|
|
45
|
+
lines.push("| ID | Name | Members |");
|
|
46
|
+
lines.push("|----|------|---------|");
|
|
47
|
+
// Table rows
|
|
48
|
+
for (const group of response.groups) {
|
|
49
|
+
const members = group.users && group.users.length > 0 ? String(group.users.length) : "-";
|
|
50
|
+
lines.push(`| ${group.id} | ${group.name} | ${members} |`);
|
|
51
|
+
}
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
export { formatJournals, type NameLookup } from "./journals.js";
|
|
2
|
-
export { formatIssue, formatIssueResponse } from "./issue.js";
|
|
2
|
+
export { formatIssue, formatIssueResponse, formatIssueList } from "./issue.js";
|
|
3
|
+
export { formatProject, formatProjectList } from "./project.js";
|
|
4
|
+
export { formatUser, formatUserList } from "./user.js";
|
|
5
|
+
export { formatSearchResults } from "./search.js";
|
|
6
|
+
export { formatWikiPage, formatWikiPageList } from "./wiki.js";
|
|
7
|
+
export { formatVersion, formatVersionList } from "./version.js";
|
|
8
|
+
export { formatTimeEntry, formatTimeEntryList } from "./time.js";
|
|
9
|
+
export { formatGroup, formatGroupList } from "./group.js";
|
|
10
|
+
export { formatTrackerList, formatStatusList, formatCategoryList, formatPriorityList, formatActivityList, formatRoleList, formatRole, } from "./metadata.js";
|
package/dist/formatters/index.js
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
export { formatJournals } from "./journals.js";
|
|
2
|
-
export { formatIssue, formatIssueResponse } from "./issue.js";
|
|
2
|
+
export { formatIssue, formatIssueResponse, formatIssueList } from "./issue.js";
|
|
3
|
+
export { formatProject, formatProjectList } from "./project.js";
|
|
4
|
+
export { formatUser, formatUserList } from "./user.js";
|
|
5
|
+
export { formatSearchResults } from "./search.js";
|
|
6
|
+
export { formatWikiPage, formatWikiPageList } from "./wiki.js";
|
|
7
|
+
export { formatVersion, formatVersionList } from "./version.js";
|
|
8
|
+
export { formatTimeEntry, formatTimeEntryList } from "./time.js";
|
|
9
|
+
export { formatGroup, formatGroupList } from "./group.js";
|
|
10
|
+
export { formatTrackerList, formatStatusList, formatCategoryList, formatPriorityList, formatActivityList, formatRoleList, formatRole, } from "./metadata.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RedmineIssue } from "../redmine/types.js";
|
|
1
|
+
import type { RedmineIssue, RedmineIssuesResponse } from "../redmine/types.js";
|
|
2
2
|
import { type NameLookup } from "./journals.js";
|
|
3
3
|
/**
|
|
4
4
|
* Format an issue as complete Markdown
|
|
@@ -10,3 +10,7 @@ export declare function formatIssue(issue: RedmineIssue, lookup?: NameLookup): s
|
|
|
10
10
|
export declare function formatIssueResponse(response: {
|
|
11
11
|
issue: RedmineIssue;
|
|
12
12
|
}, lookup?: NameLookup): string;
|
|
13
|
+
/**
|
|
14
|
+
* Format a list of issues as a Markdown table
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatIssueList(response: RedmineIssuesResponse): string;
|
package/dist/formatters/issue.js
CHANGED
|
@@ -126,3 +126,45 @@ export function formatIssue(issue, lookup = {}) {
|
|
|
126
126
|
export function formatIssueResponse(response, lookup = {}) {
|
|
127
127
|
return formatIssue(response.issue, lookup);
|
|
128
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);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Format a list of issues as a Markdown table
|
|
137
|
+
*/
|
|
138
|
+
export function formatIssueList(response) {
|
|
139
|
+
const { issues, total_count, offset, limit } = response;
|
|
140
|
+
const lines = [];
|
|
141
|
+
// Header with count
|
|
142
|
+
lines.push(`# Issues (${issues.length} of ${total_count})`);
|
|
143
|
+
lines.push("");
|
|
144
|
+
// Pagination info if needed
|
|
145
|
+
if (offset > 0 || total_count > limit) {
|
|
146
|
+
const start = offset + 1;
|
|
147
|
+
const end = offset + issues.length;
|
|
148
|
+
lines.push(`_Showing ${start}-${end} of ${total_count}_`);
|
|
149
|
+
lines.push("");
|
|
150
|
+
}
|
|
151
|
+
// Empty case
|
|
152
|
+
if (issues.length === 0) {
|
|
153
|
+
lines.push("No issues found.");
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
// Table header
|
|
157
|
+
lines.push("| ID | Subject | Status | Priority | Assigned | Updated |");
|
|
158
|
+
lines.push("|---|---|---|---|---|---|");
|
|
159
|
+
// Table rows
|
|
160
|
+
for (const issue of issues) {
|
|
161
|
+
const id = `#${issue.id}`;
|
|
162
|
+
const subject = issue.subject;
|
|
163
|
+
const status = issue.status.name;
|
|
164
|
+
const priority = issue.priority.name;
|
|
165
|
+
const assigned = issue.assigned_to?.name ?? "_(unassigned)_";
|
|
166
|
+
const updated = formatDateShort(issue.updated_on);
|
|
167
|
+
lines.push(`| ${id} | ${subject} | ${status} | ${priority} | ${assigned} | ${updated} |`);
|
|
168
|
+
}
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
interface RedmineTracker {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
interface RedmineTrackersResponse {
|
|
7
|
+
trackers: RedmineTracker[];
|
|
8
|
+
}
|
|
9
|
+
interface RedmineIssueStatus {
|
|
10
|
+
id: number;
|
|
11
|
+
name: string;
|
|
12
|
+
is_closed: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface RedmineIssueStatusesResponse {
|
|
15
|
+
issue_statuses: RedmineIssueStatus[];
|
|
16
|
+
}
|
|
17
|
+
interface RedmineCategory {
|
|
18
|
+
id: number;
|
|
19
|
+
name: string;
|
|
20
|
+
project?: {
|
|
21
|
+
id: number;
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
assigned_to?: {
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface RedmineCategoriesResponse {
|
|
30
|
+
issue_categories: RedmineCategory[];
|
|
31
|
+
}
|
|
32
|
+
interface RedminePriority {
|
|
33
|
+
id: number;
|
|
34
|
+
name: string;
|
|
35
|
+
is_default: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface RedminePrioritiesResponse {
|
|
38
|
+
issue_priorities: RedminePriority[];
|
|
39
|
+
}
|
|
40
|
+
interface RedmineActivity {
|
|
41
|
+
id: number;
|
|
42
|
+
name: string;
|
|
43
|
+
is_default: boolean;
|
|
44
|
+
active?: boolean;
|
|
45
|
+
}
|
|
46
|
+
interface RedmineActivitiesResponse {
|
|
47
|
+
time_entry_activities: RedmineActivity[];
|
|
48
|
+
}
|
|
49
|
+
interface RedmineRole {
|
|
50
|
+
id: number;
|
|
51
|
+
name: string;
|
|
52
|
+
assignable?: boolean;
|
|
53
|
+
permissions?: string[];
|
|
54
|
+
}
|
|
55
|
+
interface RedmineRolesResponse {
|
|
56
|
+
roles: RedmineRole[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Format a list of trackers as a Markdown table
|
|
60
|
+
*/
|
|
61
|
+
export declare function formatTrackerList(response: RedmineTrackersResponse): string;
|
|
62
|
+
/**
|
|
63
|
+
* Format a list of issue statuses as a Markdown table
|
|
64
|
+
*/
|
|
65
|
+
export declare function formatStatusList(response: RedmineIssueStatusesResponse): string;
|
|
66
|
+
/**
|
|
67
|
+
* Format a list of issue categories as a Markdown table
|
|
68
|
+
*/
|
|
69
|
+
export declare function formatCategoryList(response: RedmineCategoriesResponse): string;
|
|
70
|
+
/**
|
|
71
|
+
* Format a list of issue priorities as a Markdown table
|
|
72
|
+
*/
|
|
73
|
+
export declare function formatPriorityList(response: RedminePrioritiesResponse): string;
|
|
74
|
+
/**
|
|
75
|
+
* Format a list of time entry activities as a Markdown table
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatActivityList(response: RedmineActivitiesResponse): string;
|
|
78
|
+
/**
|
|
79
|
+
* Format a list of roles as a Markdown table
|
|
80
|
+
*/
|
|
81
|
+
export declare function formatRoleList(response: RedmineRolesResponse): string;
|
|
82
|
+
/**
|
|
83
|
+
* Format a single role with details as Markdown
|
|
84
|
+
*/
|
|
85
|
+
export declare function formatRole(response: {
|
|
86
|
+
role: RedmineRole;
|
|
87
|
+
}): string;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a list of trackers as a Markdown table
|
|
3
|
+
*/
|
|
4
|
+
export function formatTrackerList(response) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push("# Trackers");
|
|
7
|
+
lines.push("");
|
|
8
|
+
if (response.trackers.length === 0) {
|
|
9
|
+
lines.push("No trackers found.");
|
|
10
|
+
return lines.join("\n");
|
|
11
|
+
}
|
|
12
|
+
lines.push("| ID | Name |");
|
|
13
|
+
lines.push("|----|------|");
|
|
14
|
+
for (const tracker of response.trackers) {
|
|
15
|
+
lines.push(`| ${tracker.id} | ${tracker.name} |`);
|
|
16
|
+
}
|
|
17
|
+
return lines.join("\n");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Format a list of issue statuses as a Markdown table
|
|
21
|
+
*/
|
|
22
|
+
export function formatStatusList(response) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
lines.push("# Issue Statuses");
|
|
25
|
+
lines.push("");
|
|
26
|
+
if (response.issue_statuses.length === 0) {
|
|
27
|
+
lines.push("No statuses found.");
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
lines.push("| ID | Name | Closed |");
|
|
31
|
+
lines.push("|----|------|--------|");
|
|
32
|
+
for (const status of response.issue_statuses) {
|
|
33
|
+
const closed = status.is_closed ? "Yes" : "No";
|
|
34
|
+
lines.push(`| ${status.id} | ${status.name} | ${closed} |`);
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format a list of issue categories as a Markdown table
|
|
40
|
+
*/
|
|
41
|
+
export function formatCategoryList(response) {
|
|
42
|
+
const lines = [];
|
|
43
|
+
lines.push("# Issue Categories");
|
|
44
|
+
lines.push("");
|
|
45
|
+
if (response.issue_categories.length === 0) {
|
|
46
|
+
lines.push("No categories found.");
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
lines.push("| ID | Name | Assigned To |");
|
|
50
|
+
lines.push("|----|------|-------------|");
|
|
51
|
+
for (const category of response.issue_categories) {
|
|
52
|
+
const assignedTo = category.assigned_to?.name || "";
|
|
53
|
+
lines.push(`| ${category.id} | ${category.name} | ${assignedTo} |`);
|
|
54
|
+
}
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Format a list of issue priorities as a Markdown table
|
|
59
|
+
*/
|
|
60
|
+
export function formatPriorityList(response) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
lines.push("# Issue Priorities");
|
|
63
|
+
lines.push("");
|
|
64
|
+
if (response.issue_priorities.length === 0) {
|
|
65
|
+
lines.push("No priorities found.");
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
lines.push("| ID | Name | Default |");
|
|
69
|
+
lines.push("|----|------|---------|");
|
|
70
|
+
for (const priority of response.issue_priorities) {
|
|
71
|
+
const isDefault = priority.is_default ? "Yes" : "No";
|
|
72
|
+
lines.push(`| ${priority.id} | ${priority.name} | ${isDefault} |`);
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Format a list of time entry activities as a Markdown table
|
|
78
|
+
*/
|
|
79
|
+
export function formatActivityList(response) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push("# Time Entry Activities");
|
|
82
|
+
lines.push("");
|
|
83
|
+
if (response.time_entry_activities.length === 0) {
|
|
84
|
+
lines.push("No activities found.");
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
lines.push("| ID | Name | Default | Active |");
|
|
88
|
+
lines.push("|----|------|---------|--------|");
|
|
89
|
+
for (const activity of response.time_entry_activities) {
|
|
90
|
+
const isDefault = activity.is_default ? "Yes" : "No";
|
|
91
|
+
const isActive = activity.active === undefined ? "" : activity.active ? "Yes" : "No";
|
|
92
|
+
lines.push(`| ${activity.id} | ${activity.name} | ${isDefault} | ${isActive} |`);
|
|
93
|
+
}
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Format a list of roles as a Markdown table
|
|
98
|
+
*/
|
|
99
|
+
export function formatRoleList(response) {
|
|
100
|
+
const lines = [];
|
|
101
|
+
lines.push("# Roles");
|
|
102
|
+
lines.push("");
|
|
103
|
+
if (response.roles.length === 0) {
|
|
104
|
+
lines.push("No roles found.");
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
lines.push("| ID | Name | Assignable |");
|
|
108
|
+
lines.push("|----|------|------------|");
|
|
109
|
+
for (const role of response.roles) {
|
|
110
|
+
const assignable = role.assignable === undefined ? "" : role.assignable ? "Yes" : "No";
|
|
111
|
+
lines.push(`| ${role.id} | ${role.name} | ${assignable} |`);
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Format a single role with details as Markdown
|
|
117
|
+
*/
|
|
118
|
+
export function formatRole(response) {
|
|
119
|
+
const role = response.role;
|
|
120
|
+
const lines = [];
|
|
121
|
+
lines.push(`# ${role.name}`);
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push(`**ID:** ${role.id}`);
|
|
124
|
+
if (role.assignable !== undefined) {
|
|
125
|
+
lines.push(`**Assignable:** ${role.assignable ? "Yes" : "No"}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push("");
|
|
128
|
+
if (role.permissions && role.permissions.length > 0) {
|
|
129
|
+
lines.push("## Permissions");
|
|
130
|
+
lines.push("");
|
|
131
|
+
for (const permission of role.permissions) {
|
|
132
|
+
lines.push(`- ${permission}`);
|
|
133
|
+
}
|
|
134
|
+
lines.push("");
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n").trimEnd();
|
|
137
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineProject, RedmineProjectsResponse } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a single project as complete Markdown
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatProject(response: {
|
|
6
|
+
project: RedmineProject;
|
|
7
|
+
}): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format a list of projects as Markdown
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatProjectList(response: RedmineProjectsResponse): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const PROJECT_STATUS = {
|
|
2
|
+
1: "Active",
|
|
3
|
+
5: "Closed",
|
|
4
|
+
9: "Archived",
|
|
5
|
+
};
|
|
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
|
+
/**
|
|
13
|
+
* Format a single project as complete Markdown
|
|
14
|
+
*/
|
|
15
|
+
export function formatProject(response) {
|
|
16
|
+
const project = response.project;
|
|
17
|
+
const lines = [];
|
|
18
|
+
// Title
|
|
19
|
+
lines.push(`# ${project.name}`);
|
|
20
|
+
lines.push("");
|
|
21
|
+
// Status line
|
|
22
|
+
const statusParts = [];
|
|
23
|
+
statusParts.push(`**Identifier:** ${project.identifier}`);
|
|
24
|
+
statusParts.push(`**Status:** ${PROJECT_STATUS[project.status] || "Unknown"}`);
|
|
25
|
+
statusParts.push(`**Public:** ${project.is_public ? "Yes" : "No"}`);
|
|
26
|
+
lines.push(statusParts.join(" | "));
|
|
27
|
+
lines.push("");
|
|
28
|
+
// Description
|
|
29
|
+
if (project.description) {
|
|
30
|
+
lines.push("## Description");
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push(project.description);
|
|
33
|
+
lines.push("");
|
|
34
|
+
}
|
|
35
|
+
// Metadata table
|
|
36
|
+
lines.push("| Field | Value |");
|
|
37
|
+
lines.push("|-------|-------|");
|
|
38
|
+
if (project.parent) {
|
|
39
|
+
lines.push(`| Parent | ${project.parent.name} |`);
|
|
40
|
+
}
|
|
41
|
+
if (project.homepage) {
|
|
42
|
+
lines.push(`| Homepage | ${project.homepage} |`);
|
|
43
|
+
}
|
|
44
|
+
lines.push(`| Created | ${formatDate(project.created_on)} |`);
|
|
45
|
+
lines.push(`| Updated | ${formatDate(project.updated_on)} |`);
|
|
46
|
+
lines.push("");
|
|
47
|
+
// Trackers
|
|
48
|
+
if (project.trackers && project.trackers.length > 0) {
|
|
49
|
+
lines.push("## Trackers");
|
|
50
|
+
lines.push("");
|
|
51
|
+
for (const tracker of project.trackers) {
|
|
52
|
+
lines.push(`- ${tracker.name}`);
|
|
53
|
+
}
|
|
54
|
+
lines.push("");
|
|
55
|
+
}
|
|
56
|
+
// Enabled modules
|
|
57
|
+
if (project.enabled_modules && project.enabled_modules.length > 0) {
|
|
58
|
+
lines.push("## Enabled Modules");
|
|
59
|
+
lines.push("");
|
|
60
|
+
for (const mod of project.enabled_modules) {
|
|
61
|
+
lines.push(`- ${mod.name}`);
|
|
62
|
+
}
|
|
63
|
+
lines.push("");
|
|
64
|
+
}
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Format a list of projects as Markdown
|
|
69
|
+
*/
|
|
70
|
+
export function formatProjectList(response) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
lines.push(`# Projects (${response.total_count})`);
|
|
73
|
+
lines.push("");
|
|
74
|
+
if (response.projects.length === 0) {
|
|
75
|
+
lines.push("No projects found.");
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
// Table header
|
|
79
|
+
lines.push("| Name | Identifier | Status | Public |");
|
|
80
|
+
lines.push("|------|------------|--------|--------|");
|
|
81
|
+
for (const project of response.projects) {
|
|
82
|
+
const status = PROJECT_STATUS[project.status] || "Unknown";
|
|
83
|
+
const isPublic = project.is_public ? "Yes" : "No";
|
|
84
|
+
lines.push(`| ${project.name} | ${project.identifier} | ${status} | ${isPublic} |`);
|
|
85
|
+
}
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get icon for search result type
|
|
3
|
+
*/
|
|
4
|
+
function getTypeIcon(type) {
|
|
5
|
+
switch (type.toLowerCase()) {
|
|
6
|
+
case "issue":
|
|
7
|
+
return "\u{1F3AB}"; // ticket
|
|
8
|
+
case "wiki-page":
|
|
9
|
+
return "\u{1F4C4}"; // page
|
|
10
|
+
case "project":
|
|
11
|
+
return "\u{1F4C1}"; // folder
|
|
12
|
+
default:
|
|
13
|
+
return "\u{1F4CB}"; // clipboard
|
|
14
|
+
}
|
|
15
|
+
}
|
|
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
|
+
/**
|
|
32
|
+
* Format a single search result as Markdown
|
|
33
|
+
*/
|
|
34
|
+
function formatSearchResult(result) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
const icon = getTypeIcon(result.type);
|
|
37
|
+
// Title with icon
|
|
38
|
+
lines.push(`### ${icon} ${result.title}`);
|
|
39
|
+
// Type and date metadata
|
|
40
|
+
lines.push(`**Type:** ${result.type} | **Date:** ${formatDateShort(result.datetime)}`);
|
|
41
|
+
// Description (truncated)
|
|
42
|
+
if (result.description) {
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(truncate(result.description, 200));
|
|
45
|
+
}
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Format search results as Markdown
|
|
50
|
+
*/
|
|
51
|
+
export function formatSearchResults(response) {
|
|
52
|
+
const { results, total_count, offset } = response;
|
|
53
|
+
const lines = [];
|
|
54
|
+
// Header with count
|
|
55
|
+
lines.push(`# Search Results (${results.length} of ${total_count})`);
|
|
56
|
+
lines.push("");
|
|
57
|
+
// Offset info if applicable
|
|
58
|
+
if (offset > 0) {
|
|
59
|
+
lines.push(`_Starting from result ${offset + 1}_`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
}
|
|
62
|
+
// Empty case
|
|
63
|
+
if (results.length === 0) {
|
|
64
|
+
lines.push("No results found.");
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
// Format each result
|
|
68
|
+
for (let i = 0; i < results.length; i++) {
|
|
69
|
+
if (i > 0) {
|
|
70
|
+
lines.push("");
|
|
71
|
+
}
|
|
72
|
+
lines.push(formatSearchResult(results[i]));
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineTimeEntry, RedmineTimeEntriesResponse } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a single time entry as complete Markdown
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatTimeEntry(response: {
|
|
6
|
+
time_entry: RedmineTimeEntry;
|
|
7
|
+
}): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format a list of time entries as Markdown
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatTimeEntryList(response: RedmineTimeEntriesResponse): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a single time entry as complete Markdown
|
|
3
|
+
*/
|
|
4
|
+
export function formatTimeEntry(response) {
|
|
5
|
+
const entry = response.time_entry;
|
|
6
|
+
const lines = [];
|
|
7
|
+
// Header
|
|
8
|
+
lines.push(`# Time Entry #${entry.id}`);
|
|
9
|
+
lines.push("");
|
|
10
|
+
// Metadata table
|
|
11
|
+
lines.push("| Field | Value |");
|
|
12
|
+
lines.push("|-------|-------|");
|
|
13
|
+
lines.push(`| Project | ${entry.project.name} |`);
|
|
14
|
+
if (entry.issue) {
|
|
15
|
+
lines.push(`| Issue | #${entry.issue.id} |`);
|
|
16
|
+
}
|
|
17
|
+
lines.push(`| User | ${entry.user.name} |`);
|
|
18
|
+
lines.push(`| Activity | ${entry.activity.name} |`);
|
|
19
|
+
lines.push(`| Hours | ${entry.hours} |`);
|
|
20
|
+
lines.push(`| Date | ${entry.spent_on} |`);
|
|
21
|
+
if (entry.comments) {
|
|
22
|
+
lines.push(`| Comments | ${entry.comments} |`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("");
|
|
25
|
+
return lines.join("\n");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Format a list of time entries as Markdown
|
|
29
|
+
*/
|
|
30
|
+
export function formatTimeEntryList(response) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
lines.push(`# Time Entries (${response.total_count})`);
|
|
33
|
+
lines.push("");
|
|
34
|
+
if (response.time_entries.length === 0) {
|
|
35
|
+
lines.push("No time entries found.");
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
// Table header
|
|
39
|
+
lines.push("| ID | Date | Hours | User | Activity | Issue | Comments |");
|
|
40
|
+
lines.push("|----|------|-------|------|----------|-------|----------|");
|
|
41
|
+
for (const entry of response.time_entries) {
|
|
42
|
+
const issue = entry.issue ? `#${entry.issue.id}` : "-";
|
|
43
|
+
const comments = entry.comments || "-";
|
|
44
|
+
lines.push(`| ${entry.id} | ${entry.spent_on} | ${entry.hours} | ${entry.user.name} | ${entry.activity.name} | ${issue} | ${comments} |`);
|
|
45
|
+
}
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineUser, RedmineUsersResponse } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a single user as complete Markdown
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatUser(response: {
|
|
6
|
+
user: RedmineUser;
|
|
7
|
+
}): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format a list of users as Markdown table
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatUserList(response: RedmineUsersResponse): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const USER_STATUS = {
|
|
2
|
+
1: "Active",
|
|
3
|
+
2: "Registered",
|
|
4
|
+
3: "Locked",
|
|
5
|
+
};
|
|
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
|
+
/**
|
|
13
|
+
* Format a single user as complete Markdown
|
|
14
|
+
*/
|
|
15
|
+
export function formatUser(response) {
|
|
16
|
+
const user = response.user;
|
|
17
|
+
const lines = [];
|
|
18
|
+
// Header
|
|
19
|
+
lines.push(`# ${user.firstname} ${user.lastname}`);
|
|
20
|
+
lines.push("");
|
|
21
|
+
// Status line
|
|
22
|
+
const statusParts = [];
|
|
23
|
+
statusParts.push(`**Login:** ${user.login}`);
|
|
24
|
+
const status = user.status !== undefined ? USER_STATUS[user.status] || "Unknown" : "Unknown";
|
|
25
|
+
statusParts.push(`**Status:** ${status}`);
|
|
26
|
+
if (user.admin) {
|
|
27
|
+
statusParts.push(`**Role:** Admin`);
|
|
28
|
+
}
|
|
29
|
+
lines.push(statusParts.join(" | "));
|
|
30
|
+
lines.push("");
|
|
31
|
+
// Metadata table
|
|
32
|
+
lines.push("| Field | Value |");
|
|
33
|
+
lines.push("|-------|-------|");
|
|
34
|
+
if (user.mail) {
|
|
35
|
+
lines.push(`| Email | ${user.mail} |`);
|
|
36
|
+
}
|
|
37
|
+
lines.push(`| Created | ${formatDate(user.created_on)} |`);
|
|
38
|
+
if (user.last_login_on) {
|
|
39
|
+
lines.push(`| Last Login | ${formatDate(user.last_login_on)} |`);
|
|
40
|
+
}
|
|
41
|
+
lines.push("");
|
|
42
|
+
// Groups
|
|
43
|
+
if (user.groups && user.groups.length > 0) {
|
|
44
|
+
lines.push("## Groups");
|
|
45
|
+
lines.push("");
|
|
46
|
+
for (const group of user.groups) {
|
|
47
|
+
lines.push(`- ${group.name}`);
|
|
48
|
+
}
|
|
49
|
+
lines.push("");
|
|
50
|
+
}
|
|
51
|
+
// Project Memberships
|
|
52
|
+
if (user.memberships && user.memberships.length > 0) {
|
|
53
|
+
lines.push("## Project Memberships");
|
|
54
|
+
lines.push("");
|
|
55
|
+
for (const membership of user.memberships) {
|
|
56
|
+
const roles = membership.roles.map((r) => r.name).join(", ");
|
|
57
|
+
lines.push(`- **${membership.project.name}:** ${roles}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push("");
|
|
60
|
+
}
|
|
61
|
+
return lines.join("\n").trimEnd();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Format a list of users as Markdown table
|
|
65
|
+
*/
|
|
66
|
+
export function formatUserList(response) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
// Header
|
|
69
|
+
lines.push(`# Users (${response.total_count})`);
|
|
70
|
+
lines.push("");
|
|
71
|
+
// Empty case
|
|
72
|
+
if (response.users.length === 0) {
|
|
73
|
+
lines.push("No users found.");
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
// Table header
|
|
77
|
+
lines.push("| Name | Login | Email | Status | Role |");
|
|
78
|
+
lines.push("|------|-------|-------|--------|------|");
|
|
79
|
+
// Table rows
|
|
80
|
+
for (const user of response.users) {
|
|
81
|
+
const name = `${user.firstname} ${user.lastname}`;
|
|
82
|
+
const email = user.mail || "";
|
|
83
|
+
const status = user.status !== undefined ? USER_STATUS[user.status] || "Unknown" : "Unknown";
|
|
84
|
+
const role = user.admin ? "Admin" : "User";
|
|
85
|
+
lines.push(`| ${name} | ${user.login} | ${email} | ${status} | ${role} |`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineVersion, RedmineVersionsResponse } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a single version as complete Markdown
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatVersion(response: {
|
|
6
|
+
version: RedmineVersion;
|
|
7
|
+
}): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format a list of versions as Markdown table
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatVersionList(response: RedmineVersionsResponse): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const STATUS_BADGE = {
|
|
2
|
+
open: "Open",
|
|
3
|
+
locked: "Locked",
|
|
4
|
+
closed: "Closed",
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Format a status badge
|
|
8
|
+
*/
|
|
9
|
+
function formatStatusBadge(status) {
|
|
10
|
+
return STATUS_BADGE[status] || status;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Calculate progress percentage from estimated/spent hours
|
|
14
|
+
*/
|
|
15
|
+
function formatProgress(estimated, spent) {
|
|
16
|
+
if (!estimated || estimated === 0) {
|
|
17
|
+
return spent ? `${spent}h spent` : "-";
|
|
18
|
+
}
|
|
19
|
+
const percentage = Math.round((spent || 0) / estimated * 100);
|
|
20
|
+
return `${percentage}%`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Format a single version as complete Markdown
|
|
24
|
+
*/
|
|
25
|
+
export function formatVersion(response) {
|
|
26
|
+
const version = response.version;
|
|
27
|
+
const lines = [];
|
|
28
|
+
// Title with status badge
|
|
29
|
+
lines.push(`# ${version.name} [${formatStatusBadge(version.status)}]`);
|
|
30
|
+
lines.push("");
|
|
31
|
+
// Metadata table
|
|
32
|
+
lines.push("| Field | Value |");
|
|
33
|
+
lines.push("|-------|-------|");
|
|
34
|
+
lines.push(`| Project | ${version.project.name} |`);
|
|
35
|
+
if (version.due_date) {
|
|
36
|
+
lines.push(`| Due Date | ${version.due_date} |`);
|
|
37
|
+
}
|
|
38
|
+
lines.push(`| Status | ${formatStatusBadge(version.status)} |`);
|
|
39
|
+
if (version.estimated_hours !== undefined || version.spent_hours !== undefined) {
|
|
40
|
+
const est = version.estimated_hours !== undefined ? `${version.estimated_hours}h` : "-";
|
|
41
|
+
const spent = version.spent_hours !== undefined ? `${version.spent_hours}h` : "-";
|
|
42
|
+
lines.push(`| Est/Spent Hours | ${est} / ${spent} |`);
|
|
43
|
+
}
|
|
44
|
+
if (version.description) {
|
|
45
|
+
lines.push(`| Description | ${version.description} |`);
|
|
46
|
+
}
|
|
47
|
+
if (version.wiki_page_title) {
|
|
48
|
+
lines.push(`| Wiki Page | ${version.wiki_page_title} |`);
|
|
49
|
+
}
|
|
50
|
+
lines.push(`| Sharing | ${version.sharing} |`);
|
|
51
|
+
lines.push("");
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Format a list of versions as Markdown table
|
|
56
|
+
*/
|
|
57
|
+
export function formatVersionList(response) {
|
|
58
|
+
const lines = [];
|
|
59
|
+
lines.push(`# Versions (${response.total_count})`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
if (response.versions.length === 0) {
|
|
62
|
+
lines.push("No versions found.");
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
// Table header
|
|
66
|
+
lines.push("| Name | Status | Due Date | Progress |");
|
|
67
|
+
lines.push("|------|--------|----------|----------|");
|
|
68
|
+
for (const version of response.versions) {
|
|
69
|
+
const status = formatStatusBadge(version.status);
|
|
70
|
+
const dueDate = version.due_date || "-";
|
|
71
|
+
const progress = formatProgress(version.estimated_hours, version.spent_hours);
|
|
72
|
+
lines.push(`| ${version.name} | ${status} | ${dueDate} | ${progress} |`);
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedmineWikiPage, RedmineWikiPagesResponse } from "../redmine/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format a single wiki page as Markdown
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatWikiPage(response: {
|
|
6
|
+
wiki_page: RedmineWikiPage;
|
|
7
|
+
}): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format wiki pages list as Markdown
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatWikiPageList(response: RedmineWikiPagesResponse): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Format a single wiki page as Markdown
|
|
9
|
+
*/
|
|
10
|
+
export function formatWikiPage(response) {
|
|
11
|
+
const page = response.wiki_page;
|
|
12
|
+
const lines = [];
|
|
13
|
+
// Title
|
|
14
|
+
lines.push(`# ${page.title}`);
|
|
15
|
+
lines.push("");
|
|
16
|
+
// Metadata line
|
|
17
|
+
const metaParts = [];
|
|
18
|
+
metaParts.push(`**Version:** ${page.version}`);
|
|
19
|
+
metaParts.push(`**Author:** ${page.author.name}`);
|
|
20
|
+
metaParts.push(`**Updated:** ${formatDate(page.updated_on)}`);
|
|
21
|
+
if (page.parent) {
|
|
22
|
+
metaParts.push(`**Parent:** ${page.parent.title}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push(metaParts.join(" | "));
|
|
25
|
+
lines.push("");
|
|
26
|
+
// Separator
|
|
27
|
+
lines.push("---");
|
|
28
|
+
lines.push("");
|
|
29
|
+
// Content
|
|
30
|
+
lines.push(page.text);
|
|
31
|
+
lines.push("");
|
|
32
|
+
// Attachments
|
|
33
|
+
if (page.attachments && page.attachments.length > 0) {
|
|
34
|
+
lines.push("## Attachments");
|
|
35
|
+
lines.push("");
|
|
36
|
+
for (const att of page.attachments) {
|
|
37
|
+
lines.push(`- [${att.filename}](${att.content_url}) (${att.filesize} bytes)`);
|
|
38
|
+
}
|
|
39
|
+
lines.push("");
|
|
40
|
+
}
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build a tree structure from flat wiki page list
|
|
45
|
+
*/
|
|
46
|
+
function buildWikiTree(pages) {
|
|
47
|
+
const tree = new Map();
|
|
48
|
+
for (const page of pages) {
|
|
49
|
+
const parentTitle = page.parent?.title ?? null;
|
|
50
|
+
if (!tree.has(parentTitle)) {
|
|
51
|
+
tree.set(parentTitle, []);
|
|
52
|
+
}
|
|
53
|
+
tree.get(parentTitle).push(page);
|
|
54
|
+
}
|
|
55
|
+
return tree;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Recursively format wiki pages as tree
|
|
59
|
+
*/
|
|
60
|
+
function formatWikiTree(tree, parentTitle, indent) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
const children = tree.get(parentTitle) || [];
|
|
63
|
+
for (const page of children) {
|
|
64
|
+
const prefix = " ".repeat(indent);
|
|
65
|
+
lines.push(`${prefix}- **${page.title}** (v${page.version})`);
|
|
66
|
+
// Recursively add children
|
|
67
|
+
const childLines = formatWikiTree(tree, page.title, indent + 1);
|
|
68
|
+
lines.push(...childLines);
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format wiki pages list as Markdown
|
|
74
|
+
*/
|
|
75
|
+
export function formatWikiPageList(response) {
|
|
76
|
+
const pages = response.wiki_pages;
|
|
77
|
+
const lines = [];
|
|
78
|
+
// Header
|
|
79
|
+
lines.push(`# Wiki Pages (${pages.length})`);
|
|
80
|
+
lines.push("");
|
|
81
|
+
// Empty case
|
|
82
|
+
if (pages.length === 0) {
|
|
83
|
+
lines.push("No wiki pages found.");
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
86
|
+
// Build tree and format
|
|
87
|
+
const tree = buildWikiTree(pages);
|
|
88
|
+
const treeLines = formatWikiTree(tree, null, 0);
|
|
89
|
+
lines.push(...treeLines);
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
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.
|
|
6
|
+
version: "0.5.0",
|
|
7
7
|
});
|
|
8
8
|
registerTools(server, redmineClient, toolGroups);
|
|
9
9
|
return server;
|
package/dist/tools/admin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatUser, formatUserList, formatGroup, formatGroupList } from "../formatters/index.js";
|
|
2
3
|
export function registerAdminTools(server, client) {
|
|
3
4
|
// === USERS ===
|
|
4
5
|
server.registerTool("list_users", {
|
|
@@ -12,8 +13,13 @@ export function registerAdminTools(server, client) {
|
|
|
12
13
|
},
|
|
13
14
|
}, async (params) => {
|
|
14
15
|
const result = await client.listUsers(params);
|
|
16
|
+
if ("error" in result) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
15
21
|
return {
|
|
16
|
-
content: [{ type: "text", text:
|
|
22
|
+
content: [{ type: "text", text: formatUserList(result) }],
|
|
17
23
|
};
|
|
18
24
|
});
|
|
19
25
|
server.registerTool("get_user", {
|
|
@@ -24,8 +30,13 @@ export function registerAdminTools(server, client) {
|
|
|
24
30
|
},
|
|
25
31
|
}, async (params) => {
|
|
26
32
|
const result = await client.getUser(params.user_id, params.include);
|
|
33
|
+
if ("error" in result) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
27
38
|
return {
|
|
28
|
-
content: [{ type: "text", text:
|
|
39
|
+
content: [{ type: "text", text: formatUser(result) }],
|
|
29
40
|
};
|
|
30
41
|
});
|
|
31
42
|
server.registerTool("create_user", {
|
|
@@ -84,8 +95,13 @@ export function registerAdminTools(server, client) {
|
|
|
84
95
|
description: "List all groups (admin only)",
|
|
85
96
|
}, async () => {
|
|
86
97
|
const result = await client.listGroups();
|
|
98
|
+
if ("error" in result) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
87
103
|
return {
|
|
88
|
-
content: [{ type: "text", text:
|
|
104
|
+
content: [{ type: "text", text: formatGroupList(result) }],
|
|
89
105
|
};
|
|
90
106
|
});
|
|
91
107
|
server.registerTool("get_group", {
|
|
@@ -96,8 +112,13 @@ export function registerAdminTools(server, client) {
|
|
|
96
112
|
},
|
|
97
113
|
}, async (params) => {
|
|
98
114
|
const result = await client.getGroup(params.group_id, params.include);
|
|
115
|
+
if ("error" in result) {
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
99
120
|
return {
|
|
100
|
-
content: [{ type: "text", text:
|
|
121
|
+
content: [{ type: "text", text: formatGroup(result) }],
|
|
101
122
|
};
|
|
102
123
|
});
|
|
103
124
|
server.registerTool("create_group", {
|
package/dist/tools/core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatIssueResponse } from "../formatters/index.js";
|
|
2
|
+
import { formatIssueResponse, formatIssueList, formatProject, formatProjectList } from "../formatters/index.js";
|
|
3
3
|
export function registerCoreTools(server, client) {
|
|
4
4
|
// === ISSUES ===
|
|
5
5
|
server.registerTool("list_issues", {
|
|
@@ -24,8 +24,13 @@ export function registerCoreTools(server, client) {
|
|
|
24
24
|
},
|
|
25
25
|
}, async (params) => {
|
|
26
26
|
const result = await client.listIssues(params);
|
|
27
|
+
if ("error" in result) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
27
32
|
return {
|
|
28
|
-
content: [{ type: "text", text:
|
|
33
|
+
content: [{ type: "text", text: formatIssueList(result) }],
|
|
29
34
|
};
|
|
30
35
|
});
|
|
31
36
|
server.registerTool("get_issue", {
|
|
@@ -181,8 +186,13 @@ export function registerCoreTools(server, client) {
|
|
|
181
186
|
},
|
|
182
187
|
}, async (params) => {
|
|
183
188
|
const result = await client.listProjects(params);
|
|
189
|
+
if ("error" in result) {
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
184
194
|
return {
|
|
185
|
-
content: [{ type: "text", text:
|
|
195
|
+
content: [{ type: "text", text: formatProjectList(result) }],
|
|
186
196
|
};
|
|
187
197
|
});
|
|
188
198
|
server.registerTool("get_project", {
|
|
@@ -193,8 +203,13 @@ export function registerCoreTools(server, client) {
|
|
|
193
203
|
},
|
|
194
204
|
}, async (params) => {
|
|
195
205
|
const result = await client.getProject(params.project_id, params.include);
|
|
206
|
+
if ("error" in result) {
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
196
211
|
return {
|
|
197
|
-
content: [{ type: "text", text:
|
|
212
|
+
content: [{ type: "text", text: formatProject(result) }],
|
|
198
213
|
};
|
|
199
214
|
});
|
|
200
215
|
server.registerTool("create_project", {
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
import { formatPriorityList, formatActivityList } from "../formatters/index.js";
|
|
1
2
|
export function registerEnumerationsTools(server, client) {
|
|
2
3
|
server.registerTool("list_issue_priorities", {
|
|
3
4
|
description: "List all issue priorities with their IDs (Low, Normal, High, Urgent, Immediate)",
|
|
4
5
|
}, async () => {
|
|
5
6
|
const result = await client.listIssuePriorities();
|
|
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:
|
|
13
|
+
content: [{ type: "text", text: formatPriorityList(result) }],
|
|
8
14
|
};
|
|
9
15
|
});
|
|
10
16
|
server.registerTool("list_time_entry_activities", {
|
|
11
17
|
description: "List all time entry activities with their IDs (Design, Development, etc.)",
|
|
12
18
|
}, async () => {
|
|
13
19
|
const result = await client.listTimeEntryActivities();
|
|
20
|
+
if ("error" in result) {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
14
25
|
return {
|
|
15
|
-
content: [{ type: "text", text:
|
|
26
|
+
content: [{ type: "text", text: formatActivityList(result) }],
|
|
16
27
|
};
|
|
17
28
|
});
|
|
18
29
|
server.registerTool("list_document_categories", {
|
package/dist/tools/metadata.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatTrackerList, formatStatusList, formatCategoryList } from "../formatters/index.js";
|
|
2
3
|
export function registerMetadataTools(server, client) {
|
|
3
4
|
// === TRACKERS ===
|
|
4
5
|
server.registerTool("list_trackers", {
|
|
5
6
|
description: "List all available trackers (issue types like Bug, Feature, etc.)",
|
|
6
7
|
}, async () => {
|
|
7
8
|
const result = await client.listTrackers();
|
|
9
|
+
if ("error" in result) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
8
14
|
return {
|
|
9
|
-
content: [{ type: "text", text:
|
|
15
|
+
content: [{ type: "text", text: formatTrackerList(result) }],
|
|
10
16
|
};
|
|
11
17
|
});
|
|
12
18
|
// === ISSUE STATUSES ===
|
|
@@ -14,8 +20,13 @@ export function registerMetadataTools(server, client) {
|
|
|
14
20
|
description: "List all available issue statuses (New, In Progress, Closed, etc.)",
|
|
15
21
|
}, async () => {
|
|
16
22
|
const result = await client.listIssueStatuses();
|
|
23
|
+
if ("error" in result) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
17
28
|
return {
|
|
18
|
-
content: [{ type: "text", text:
|
|
29
|
+
content: [{ type: "text", text: formatStatusList(result) }],
|
|
19
30
|
};
|
|
20
31
|
});
|
|
21
32
|
// === ISSUE CATEGORIES ===
|
|
@@ -26,8 +37,13 @@ export function registerMetadataTools(server, client) {
|
|
|
26
37
|
},
|
|
27
38
|
}, async (params) => {
|
|
28
39
|
const result = await client.listIssueCategories(params.project_id);
|
|
40
|
+
if ("error" in result) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
29
45
|
return {
|
|
30
|
-
content: [{ type: "text", text:
|
|
46
|
+
content: [{ type: "text", text: formatCategoryList(result) }],
|
|
31
47
|
};
|
|
32
48
|
});
|
|
33
49
|
server.registerTool("get_issue_category", {
|
package/dist/tools/relations.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatVersion, formatVersionList } from "../formatters/index.js";
|
|
2
3
|
export function registerRelationsTools(server, client) {
|
|
3
4
|
// === ISSUE RELATIONS ===
|
|
4
5
|
server.registerTool("list_issue_relations", {
|
|
@@ -76,8 +77,13 @@ export function registerRelationsTools(server, client) {
|
|
|
76
77
|
},
|
|
77
78
|
}, async (params) => {
|
|
78
79
|
const result = await client.listVersions(params.project_id);
|
|
80
|
+
if ("error" in result) {
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
79
85
|
return {
|
|
80
|
-
content: [{ type: "text", text:
|
|
86
|
+
content: [{ type: "text", text: formatVersionList(result) }],
|
|
81
87
|
};
|
|
82
88
|
});
|
|
83
89
|
server.registerTool("get_version", {
|
|
@@ -87,8 +93,13 @@ export function registerRelationsTools(server, client) {
|
|
|
87
93
|
},
|
|
88
94
|
}, async (params) => {
|
|
89
95
|
const result = await client.getVersion(params.version_id);
|
|
96
|
+
if ("error" in result) {
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
90
101
|
return {
|
|
91
|
-
content: [{ type: "text", text:
|
|
102
|
+
content: [{ type: "text", text: formatVersion(result) }],
|
|
92
103
|
};
|
|
93
104
|
});
|
|
94
105
|
server.registerTool("create_version", {
|
package/dist/tools/roles.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatRoleList, formatRole } from "../formatters/index.js";
|
|
2
3
|
export function registerRolesTools(server, client) {
|
|
3
4
|
server.registerTool("list_roles", {
|
|
4
5
|
description: "List all available roles (Manager, Developer, Reporter, etc.)",
|
|
5
6
|
}, async () => {
|
|
6
7
|
const result = await client.listRoles();
|
|
8
|
+
if ("error" in result) {
|
|
9
|
+
return {
|
|
10
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
7
13
|
return {
|
|
8
|
-
content: [{ type: "text", text:
|
|
14
|
+
content: [{ type: "text", text: formatRoleList(result) }],
|
|
9
15
|
};
|
|
10
16
|
});
|
|
11
17
|
server.registerTool("get_role", {
|
|
@@ -15,8 +21,13 @@ export function registerRolesTools(server, client) {
|
|
|
15
21
|
},
|
|
16
22
|
}, async (params) => {
|
|
17
23
|
const result = await client.getRole(params.role_id);
|
|
24
|
+
if ("error" in result) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
18
29
|
return {
|
|
19
|
-
content: [{ type: "text", text:
|
|
30
|
+
content: [{ type: "text", text: formatRole(result) }],
|
|
20
31
|
};
|
|
21
32
|
});
|
|
22
33
|
}
|
package/dist/tools/search.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatSearchResults } from "../formatters/index.js";
|
|
2
3
|
export function registerSearchTools(server, client) {
|
|
3
4
|
server.registerTool("search", {
|
|
4
5
|
description: "Search across Redmine (issues, wiki, news, etc.)",
|
|
@@ -21,8 +22,13 @@ export function registerSearchTools(server, client) {
|
|
|
21
22
|
},
|
|
22
23
|
}, async (params) => {
|
|
23
24
|
const result = await client.search(params);
|
|
25
|
+
if ("error" in result) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
24
30
|
return {
|
|
25
|
-
content: [{ type: "text", text:
|
|
31
|
+
content: [{ type: "text", text: formatSearchResults(result) }],
|
|
26
32
|
};
|
|
27
33
|
});
|
|
28
34
|
}
|
package/dist/tools/time.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatTimeEntry, formatTimeEntryList } from "../formatters/index.js";
|
|
2
3
|
export function registerTimeTools(server, client) {
|
|
3
4
|
server.registerTool("list_time_entries", {
|
|
4
5
|
description: "List time entries with optional filters",
|
|
@@ -13,8 +14,13 @@ export function registerTimeTools(server, client) {
|
|
|
13
14
|
},
|
|
14
15
|
}, async (params) => {
|
|
15
16
|
const result = await client.listTimeEntries(params);
|
|
17
|
+
if ("error" in result) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
16
22
|
return {
|
|
17
|
-
content: [{ type: "text", text:
|
|
23
|
+
content: [{ type: "text", text: formatTimeEntryList(result) }],
|
|
18
24
|
};
|
|
19
25
|
});
|
|
20
26
|
server.registerTool("get_time_entry", {
|
|
@@ -24,8 +30,13 @@ export function registerTimeTools(server, client) {
|
|
|
24
30
|
},
|
|
25
31
|
}, async (params) => {
|
|
26
32
|
const result = await client.getTimeEntry(params.time_entry_id);
|
|
33
|
+
if ("error" in result) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
27
38
|
return {
|
|
28
|
-
content: [{ type: "text", text:
|
|
39
|
+
content: [{ type: "text", text: formatTimeEntry(result) }],
|
|
29
40
|
};
|
|
30
41
|
});
|
|
31
42
|
server.registerTool("create_time_entry", {
|
package/dist/tools/wiki.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatWikiPage, formatWikiPageList } from "../formatters/index.js";
|
|
2
3
|
export function registerWikiTools(server, client) {
|
|
3
4
|
server.registerTool("list_wiki_pages", {
|
|
4
5
|
description: "List all wiki pages in a project",
|
|
@@ -7,8 +8,13 @@ export function registerWikiTools(server, client) {
|
|
|
7
8
|
},
|
|
8
9
|
}, async (params) => {
|
|
9
10
|
const result = await client.listWikiPages(params.project_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:
|
|
17
|
+
content: [{ type: "text", text: formatWikiPageList(result) }],
|
|
12
18
|
};
|
|
13
19
|
});
|
|
14
20
|
server.registerTool("get_wiki_page", {
|
|
@@ -24,8 +30,13 @@ export function registerWikiTools(server, client) {
|
|
|
24
30
|
version: params.version,
|
|
25
31
|
include: params.include,
|
|
26
32
|
});
|
|
33
|
+
if ("error" in result) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
27
38
|
return {
|
|
28
|
-
content: [{ type: "text", text:
|
|
39
|
+
content: [{ type: "text", text: formatWikiPage(result) }],
|
|
29
40
|
};
|
|
30
41
|
});
|
|
31
42
|
server.registerTool("create_wiki_page", {
|