@pschroee/redmine-mcp 0.3.1
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/README.md +360 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +99 -0
- package/dist/redmine/client.d.ts +349 -0
- package/dist/redmine/client.js +458 -0
- package/dist/redmine/types.d.ts +489 -0
- package/dist/redmine/types.js +2 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +10 -0
- package/dist/tools/account.d.ts +3 -0
- package/dist/tools/account.js +10 -0
- package/dist/tools/admin.d.ts +3 -0
- package/dist/tools/admin.js +150 -0
- package/dist/tools/core.d.ts +3 -0
- package/dist/tools/core.js +242 -0
- package/dist/tools/enumerations.d.ts +3 -0
- package/dist/tools/enumerations.js +26 -0
- package/dist/tools/files.d.ts +3 -0
- package/dist/tools/files.js +70 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +59 -0
- package/dist/tools/issues.d.ts +3 -0
- package/dist/tools/issues.js +61 -0
- package/dist/tools/memberships.d.ts +3 -0
- package/dist/tools/memberships.js +66 -0
- package/dist/tools/metadata.d.ts +3 -0
- package/dist/tools/metadata.js +102 -0
- package/dist/tools/projects.d.ts +3 -0
- package/dist/tools/projects.js +49 -0
- package/dist/tools/relations.d.ts +3 -0
- package/dist/tools/relations.js +157 -0
- package/dist/tools/roles.d.ts +3 -0
- package/dist/tools/roles.js +22 -0
- package/dist/tools/search.d.ts +3 -0
- package/dist/tools/search.js +28 -0
- package/dist/tools/time.d.ts +3 -0
- package/dist/tools/time.js +75 -0
- package/dist/tools/wiki.d.ts +3 -0
- package/dist/tools/wiki.js +75 -0
- package/package.json +55 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerCoreTools(server, client) {
|
|
3
|
+
// === ISSUES ===
|
|
4
|
+
server.registerTool("list_issues", {
|
|
5
|
+
description: "List issues from Redmine with optional filters and sorting",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
project_id: z.union([z.string(), z.number()]).optional().describe("Filter by project ID or identifier"),
|
|
8
|
+
tracker_id: z.number().optional().describe("Filter by tracker ID"),
|
|
9
|
+
status_id: z.union([z.string(), z.number()]).optional().describe("Filter by status: 'open', 'closed', '*', or status ID"),
|
|
10
|
+
assigned_to_id: z.union([z.number(), z.string()]).optional().describe("Filter by assigned user ID or 'me'"),
|
|
11
|
+
author_id: z.union([z.number(), z.string()]).optional().describe("Filter by author user ID or 'me'"),
|
|
12
|
+
category_id: z.number().optional().describe("Filter by category ID"),
|
|
13
|
+
fixed_version_id: z.number().optional().describe("Filter by version ID"),
|
|
14
|
+
parent_id: z.number().optional().describe("Filter by parent issue ID"),
|
|
15
|
+
subject: z.string().optional().describe("Filter by subject (use ~ for contains)"),
|
|
16
|
+
created_on: z.string().optional().describe("Filter by created date (e.g., '>=2023-01-01', '><2023-01-01|2023-12-31')"),
|
|
17
|
+
updated_on: z.string().optional().describe("Filter by updated date"),
|
|
18
|
+
sort: z.string().optional().describe("Sort by field:direction (e.g., 'updated_on:desc,priority:asc')"),
|
|
19
|
+
include: z.string().optional().describe("Include associated data: attachments, relations, journals, watchers, children"),
|
|
20
|
+
limit: z.number().optional().describe("Maximum results (default 25, max 100)"),
|
|
21
|
+
offset: z.number().optional().describe("Skip first N results"),
|
|
22
|
+
},
|
|
23
|
+
}, async (params) => {
|
|
24
|
+
const result = await client.listIssues(params);
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
server.registerTool("get_issue", {
|
|
30
|
+
description: "Get details of a specific issue by ID",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
issue_id: z.number().describe("The issue ID"),
|
|
33
|
+
include: z.string().optional().describe("Include: attachments, relations, journals, watchers, children, changesets, allowed_statuses"),
|
|
34
|
+
},
|
|
35
|
+
}, async (params) => {
|
|
36
|
+
const result = await client.getIssue(params.issue_id, params.include);
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
server.registerTool("create_issue", {
|
|
42
|
+
description: "Create a new issue in Redmine",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier"),
|
|
45
|
+
subject: z.string().describe("Issue subject/title"),
|
|
46
|
+
description: z.string().optional().describe("Issue description (supports Textile/Markdown)"),
|
|
47
|
+
tracker_id: z.number().optional().describe("Tracker ID (e.g., Bug, Feature)"),
|
|
48
|
+
status_id: z.number().optional().describe("Status ID"),
|
|
49
|
+
priority_id: z.number().optional().describe("Priority ID"),
|
|
50
|
+
assigned_to_id: z.number().optional().describe("User ID to assign"),
|
|
51
|
+
category_id: z.number().optional().describe("Category ID"),
|
|
52
|
+
fixed_version_id: z.number().optional().describe("Target version ID"),
|
|
53
|
+
parent_issue_id: z.number().optional().describe("Parent issue ID for subtasks"),
|
|
54
|
+
custom_fields: z.array(z.object({
|
|
55
|
+
id: z.number(),
|
|
56
|
+
value: z.union([z.string(), z.array(z.string())]),
|
|
57
|
+
})).optional().describe("Custom field values"),
|
|
58
|
+
watcher_user_ids: z.array(z.number()).optional().describe("User IDs to add as watchers"),
|
|
59
|
+
is_private: z.boolean().optional().describe("Make issue private"),
|
|
60
|
+
estimated_hours: z.number().optional().describe("Estimated hours"),
|
|
61
|
+
start_date: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
62
|
+
due_date: z.string().optional().describe("Due date (YYYY-MM-DD)"),
|
|
63
|
+
},
|
|
64
|
+
}, async (params) => {
|
|
65
|
+
const result = await client.createIssue(params);
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
server.registerTool("update_issue", {
|
|
71
|
+
description: "Update an existing issue",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
issue_id: z.number().describe("The issue ID to update"),
|
|
74
|
+
subject: z.string().optional().describe("New subject/title"),
|
|
75
|
+
description: z.string().optional().describe("New description"),
|
|
76
|
+
project_id: z.union([z.string(), z.number()]).optional().describe("Move to different project"),
|
|
77
|
+
tracker_id: z.number().optional().describe("Change tracker"),
|
|
78
|
+
status_id: z.number().optional().describe("Change status"),
|
|
79
|
+
priority_id: z.number().optional().describe("Change priority"),
|
|
80
|
+
assigned_to_id: z.union([z.number(), z.string()]).optional().describe("Change assignee (ID or '' to unassign)"),
|
|
81
|
+
category_id: z.number().optional().describe("Change category"),
|
|
82
|
+
fixed_version_id: z.union([z.number(), z.string()]).optional().describe("Change target version (ID or '' to clear)"),
|
|
83
|
+
parent_issue_id: z.union([z.number(), z.string()]).optional().describe("Change parent (ID or '' to clear)"),
|
|
84
|
+
custom_fields: z.array(z.object({
|
|
85
|
+
id: z.number(),
|
|
86
|
+
value: z.union([z.string(), z.array(z.string())]),
|
|
87
|
+
})).optional().describe("Update custom fields"),
|
|
88
|
+
notes: z.string().optional().describe("Add a comment/note to the issue"),
|
|
89
|
+
private_notes: z.boolean().optional().describe("Make the note private"),
|
|
90
|
+
is_private: z.boolean().optional().describe("Change private flag"),
|
|
91
|
+
estimated_hours: z.number().optional().describe("Update estimated hours"),
|
|
92
|
+
done_ratio: z.number().optional().describe("Update % done (0-100). Note: For parent issues with children, this may be calculated automatically depending on Redmine configuration"),
|
|
93
|
+
start_date: z.string().optional().describe("Update start date"),
|
|
94
|
+
due_date: z.string().optional().describe("Update due date"),
|
|
95
|
+
},
|
|
96
|
+
}, async (params) => {
|
|
97
|
+
const { issue_id, ...data } = params;
|
|
98
|
+
const result = await client.updateIssue(issue_id, data);
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
server.registerTool("delete_issue", {
|
|
104
|
+
description: "Delete an issue permanently",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
issue_id: z.number().describe("The issue ID to delete"),
|
|
107
|
+
},
|
|
108
|
+
}, async (params) => {
|
|
109
|
+
const result = await client.deleteIssue(params.issue_id);
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
server.registerTool("add_issue_watcher", {
|
|
115
|
+
description: "Add a user as watcher to an issue",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
issue_id: z.number().describe("The issue ID"),
|
|
118
|
+
user_id: z.number().describe("The user ID to add as watcher"),
|
|
119
|
+
},
|
|
120
|
+
}, async (params) => {
|
|
121
|
+
const result = await client.addIssueWatcher(params.issue_id, params.user_id);
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
server.registerTool("remove_issue_watcher", {
|
|
127
|
+
description: "Remove a user from issue watchers",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
issue_id: z.number().describe("The issue ID"),
|
|
130
|
+
user_id: z.number().describe("The user ID to remove from watchers"),
|
|
131
|
+
},
|
|
132
|
+
}, async (params) => {
|
|
133
|
+
const result = await client.removeIssueWatcher(params.issue_id, params.user_id);
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
// === PROJECTS ===
|
|
139
|
+
server.registerTool("list_projects", {
|
|
140
|
+
description: "List all accessible projects from Redmine",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
include: z.string().optional().describe("Include: trackers, issue_categories, enabled_modules, time_entry_activities, issue_custom_fields"),
|
|
143
|
+
limit: z.number().optional().describe("Maximum results (default 25, max 100)"),
|
|
144
|
+
offset: z.number().optional().describe("Skip first N results"),
|
|
145
|
+
},
|
|
146
|
+
}, async (params) => {
|
|
147
|
+
const result = await client.listProjects(params);
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
server.registerTool("get_project", {
|
|
153
|
+
description: "Get details of a specific project",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier"),
|
|
156
|
+
include: z.string().optional().describe("Include: trackers, issue_categories, enabled_modules, time_entry_activities, issue_custom_fields"),
|
|
157
|
+
},
|
|
158
|
+
}, async (params) => {
|
|
159
|
+
const result = await client.getProject(params.project_id, params.include);
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
server.registerTool("create_project", {
|
|
165
|
+
description: "Create a new project in Redmine",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
name: z.string().describe("Project name"),
|
|
168
|
+
identifier: z.string().describe("Unique identifier (lowercase, no spaces, used in URLs)"),
|
|
169
|
+
description: z.string().optional().describe("Project description"),
|
|
170
|
+
homepage: z.string().optional().describe("Project homepage URL"),
|
|
171
|
+
is_public: z.boolean().optional().describe("Whether project is public (default true)"),
|
|
172
|
+
parent_id: z.union([z.string(), z.number()]).optional().describe("Parent project ID or identifier"),
|
|
173
|
+
inherit_members: z.boolean().optional().describe("Inherit members from parent project"),
|
|
174
|
+
default_assigned_to_id: z.number().optional().describe("Default assignee user ID"),
|
|
175
|
+
default_version_id: z.number().optional().describe("Default version ID"),
|
|
176
|
+
tracker_ids: z.array(z.number()).optional().describe("Enabled tracker IDs"),
|
|
177
|
+
enabled_module_names: z.array(z.string()).optional().describe("Enabled module names"),
|
|
178
|
+
issue_custom_field_ids: z.array(z.number()).optional().describe("Enabled custom field IDs"),
|
|
179
|
+
},
|
|
180
|
+
}, async (params) => {
|
|
181
|
+
const result = await client.createProject(params);
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
server.registerTool("update_project", {
|
|
187
|
+
description: "Update an existing project",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier"),
|
|
190
|
+
name: z.string().optional().describe("New project name"),
|
|
191
|
+
description: z.string().optional().describe("New description"),
|
|
192
|
+
homepage: z.string().optional().describe("New homepage URL"),
|
|
193
|
+
is_public: z.boolean().optional().describe("Change public visibility"),
|
|
194
|
+
parent_id: z.union([z.string(), z.number()]).optional().describe("Change parent project"),
|
|
195
|
+
inherit_members: z.boolean().optional().describe("Change inherit members"),
|
|
196
|
+
default_assigned_to_id: z.number().optional().describe("Change default assignee"),
|
|
197
|
+
default_version_id: z.number().optional().describe("Change default version"),
|
|
198
|
+
tracker_ids: z.array(z.number()).optional().describe("Update enabled trackers"),
|
|
199
|
+
enabled_module_names: z.array(z.string()).optional().describe("Update enabled modules"),
|
|
200
|
+
issue_custom_field_ids: z.array(z.number()).optional().describe("Update enabled custom fields"),
|
|
201
|
+
},
|
|
202
|
+
}, async (params) => {
|
|
203
|
+
const { project_id, ...data } = params;
|
|
204
|
+
const result = await client.updateProject(project_id, data);
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
server.registerTool("delete_project", {
|
|
210
|
+
description: "Delete a project permanently (requires admin privileges)",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier to delete"),
|
|
213
|
+
},
|
|
214
|
+
}, async (params) => {
|
|
215
|
+
const result = await client.deleteProject(params.project_id);
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
server.registerTool("archive_project", {
|
|
221
|
+
description: "Archive a project (Redmine 5.0+)",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier to archive"),
|
|
224
|
+
},
|
|
225
|
+
}, async (params) => {
|
|
226
|
+
const result = await client.archiveProject(params.project_id);
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
server.registerTool("unarchive_project", {
|
|
232
|
+
description: "Unarchive a project (Redmine 5.0+)",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier to unarchive"),
|
|
235
|
+
},
|
|
236
|
+
}, async (params) => {
|
|
237
|
+
const result = await client.unarchiveProject(params.project_id);
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function registerEnumerationsTools(server, client) {
|
|
2
|
+
server.registerTool("list_issue_priorities", {
|
|
3
|
+
description: "List all issue priorities with their IDs (Low, Normal, High, Urgent, Immediate)",
|
|
4
|
+
}, async () => {
|
|
5
|
+
const result = await client.listIssuePriorities();
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
8
|
+
};
|
|
9
|
+
});
|
|
10
|
+
server.registerTool("list_time_entry_activities", {
|
|
11
|
+
description: "List all time entry activities with their IDs (Design, Development, etc.)",
|
|
12
|
+
}, async () => {
|
|
13
|
+
const result = await client.listTimeEntryActivities();
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
server.registerTool("list_document_categories", {
|
|
19
|
+
description: "List all document categories with their IDs",
|
|
20
|
+
}, async () => {
|
|
21
|
+
const result = await client.listDocumentCategories();
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
export function registerFilesTools(server, client) {
|
|
4
|
+
// === ATTACHMENTS ===
|
|
5
|
+
server.registerTool("get_attachment", {
|
|
6
|
+
description: "Get metadata of a specific attachment",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
attachment_id: z.number().describe("The attachment ID"),
|
|
9
|
+
},
|
|
10
|
+
}, async (params) => {
|
|
11
|
+
const result = await client.getAttachment(params.attachment_id);
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
server.registerTool("delete_attachment", {
|
|
17
|
+
description: "Delete an attachment",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
attachment_id: z.number().describe("The attachment ID to delete"),
|
|
20
|
+
},
|
|
21
|
+
}, async (params) => {
|
|
22
|
+
const result = await client.deleteAttachment(params.attachment_id);
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
server.registerTool("upload_file", {
|
|
28
|
+
description: "Upload a file to Redmine (returns token for attaching to issues/wiki)",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
file_path: z.string().describe("Local file path to upload"),
|
|
31
|
+
filename: z.string().describe("Filename to use in Redmine"),
|
|
32
|
+
content_type: z.string().optional().describe("MIME type (auto-detected if not provided)"),
|
|
33
|
+
},
|
|
34
|
+
}, async (params) => {
|
|
35
|
+
const content = await readFile(params.file_path);
|
|
36
|
+
const contentType = params.content_type || "application/octet-stream";
|
|
37
|
+
const result = await client.uploadFile(params.filename, contentType, content);
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
// === PROJECT FILES ===
|
|
43
|
+
server.registerTool("list_project_files", {
|
|
44
|
+
description: "List all files attached to a project",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier"),
|
|
47
|
+
},
|
|
48
|
+
}, async (params) => {
|
|
49
|
+
const result = await client.listProjectFiles(params.project_id);
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
server.registerTool("upload_project_file", {
|
|
55
|
+
description: "Attach an uploaded file to a project",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier"),
|
|
58
|
+
token: z.string().describe("Upload token from upload_file"),
|
|
59
|
+
version_id: z.number().optional().describe("Associated version ID"),
|
|
60
|
+
filename: z.string().optional().describe("Override filename"),
|
|
61
|
+
description: z.string().optional().describe("File description"),
|
|
62
|
+
},
|
|
63
|
+
}, async (params) => {
|
|
64
|
+
const { project_id, ...data } = params;
|
|
65
|
+
const result = await client.uploadProjectFile(project_id, data);
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { RedmineClient } from "../redmine/client.js";
|
|
3
|
+
export type ToolRegistrationFn = (server: McpServer, client: RedmineClient) => void;
|
|
4
|
+
export declare const toolGroups: Record<string, ToolRegistrationFn>;
|
|
5
|
+
export type ToolGroup = keyof typeof toolGroups;
|
|
6
|
+
export declare const ALL_GROUPS: ToolGroup[];
|
|
7
|
+
export declare function isValidToolGroup(group: string): group is ToolGroup;
|
|
8
|
+
export declare function validateToolGroups(groups: string[]): ToolGroup[];
|
|
9
|
+
export declare function resolveGroups(include?: string[], exclude?: string[]): ToolGroup[];
|
|
10
|
+
export declare function registerTools(server: McpServer, client: RedmineClient, groups: ToolGroup[]): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { registerAccountTools } from "./account.js";
|
|
2
|
+
import { registerAdminTools } from "./admin.js";
|
|
3
|
+
import { registerCoreTools } from "./core.js";
|
|
4
|
+
import { registerEnumerationsTools } from "./enumerations.js";
|
|
5
|
+
import { registerFilesTools } from "./files.js";
|
|
6
|
+
import { registerMembershipsTools } from "./memberships.js";
|
|
7
|
+
import { registerMetadataTools } from "./metadata.js";
|
|
8
|
+
import { registerRelationsTools } from "./relations.js";
|
|
9
|
+
import { registerRolesTools } from "./roles.js";
|
|
10
|
+
import { registerSearchTools } from "./search.js";
|
|
11
|
+
import { registerTimeTools } from "./time.js";
|
|
12
|
+
import { registerWikiTools } from "./wiki.js";
|
|
13
|
+
export const toolGroups = {
|
|
14
|
+
core: registerCoreTools,
|
|
15
|
+
metadata: registerMetadataTools,
|
|
16
|
+
wiki: registerWikiTools,
|
|
17
|
+
files: registerFilesTools,
|
|
18
|
+
relations: registerRelationsTools,
|
|
19
|
+
search: registerSearchTools,
|
|
20
|
+
account: registerAccountTools,
|
|
21
|
+
time: registerTimeTools,
|
|
22
|
+
enumerations: registerEnumerationsTools,
|
|
23
|
+
memberships: registerMembershipsTools,
|
|
24
|
+
roles: registerRolesTools,
|
|
25
|
+
admin: registerAdminTools,
|
|
26
|
+
};
|
|
27
|
+
export const ALL_GROUPS = Object.keys(toolGroups);
|
|
28
|
+
export function isValidToolGroup(group) {
|
|
29
|
+
return group in toolGroups;
|
|
30
|
+
}
|
|
31
|
+
export function validateToolGroups(groups) {
|
|
32
|
+
const invalid = groups.filter((g) => !isValidToolGroup(g));
|
|
33
|
+
if (invalid.length > 0) {
|
|
34
|
+
throw new Error(`Invalid tool group(s): ${invalid.join(", ")}. Valid groups: ${ALL_GROUPS.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
return groups;
|
|
37
|
+
}
|
|
38
|
+
export function resolveGroups(include, exclude) {
|
|
39
|
+
let groups;
|
|
40
|
+
if (include && include.length > 0) {
|
|
41
|
+
groups = validateToolGroups(include);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
groups = [...ALL_GROUPS];
|
|
45
|
+
}
|
|
46
|
+
if (exclude && exclude.length > 0) {
|
|
47
|
+
const excludeSet = new Set(validateToolGroups(exclude));
|
|
48
|
+
groups = groups.filter((g) => !excludeSet.has(g));
|
|
49
|
+
}
|
|
50
|
+
if (groups.length === 0) {
|
|
51
|
+
throw new Error("No tool groups selected. At least one group must be enabled.");
|
|
52
|
+
}
|
|
53
|
+
return groups;
|
|
54
|
+
}
|
|
55
|
+
export function registerTools(server, client, groups) {
|
|
56
|
+
for (const group of groups) {
|
|
57
|
+
toolGroups[group](server, client);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerIssueTools(server, client) {
|
|
3
|
+
server.tool("list_issues", "List issues from Redmine with optional filters", {
|
|
4
|
+
project_id: z
|
|
5
|
+
.union([z.string(), z.number()])
|
|
6
|
+
.optional()
|
|
7
|
+
.describe("Filter by project ID or identifier"),
|
|
8
|
+
status_id: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Filter by status: open, closed, or status ID"),
|
|
12
|
+
assigned_to_id: z
|
|
13
|
+
.number()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Filter by assigned user ID"),
|
|
16
|
+
limit: z.number().optional().describe("Maximum results (default 25, max 100)"),
|
|
17
|
+
offset: z.number().optional().describe("Skip first N results"),
|
|
18
|
+
}, async (params) => {
|
|
19
|
+
const result = await client.listIssues(params);
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
server.tool("get_issue", "Get details of a specific issue by ID", {
|
|
25
|
+
issue_id: z.number().describe("The issue ID"),
|
|
26
|
+
}, async (params) => {
|
|
27
|
+
const result = await client.getIssue(params.issue_id);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
server.tool("create_issue", "Create a new issue in Redmine", {
|
|
33
|
+
project_id: z
|
|
34
|
+
.union([z.string(), z.number()])
|
|
35
|
+
.describe("Project ID or identifier"),
|
|
36
|
+
subject: z.string().describe("Issue subject/title"),
|
|
37
|
+
description: z.string().optional().describe("Issue description"),
|
|
38
|
+
tracker_id: z.number().optional().describe("Tracker ID (e.g., Bug, Feature)"),
|
|
39
|
+
priority_id: z.number().optional().describe("Priority ID"),
|
|
40
|
+
assigned_to_id: z.number().optional().describe("User ID to assign"),
|
|
41
|
+
}, async (params) => {
|
|
42
|
+
const result = await client.createIssue(params);
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
server.tool("update_issue", "Update an existing issue", {
|
|
48
|
+
issue_id: z.number().describe("The issue ID to update"),
|
|
49
|
+
subject: z.string().optional().describe("New subject/title"),
|
|
50
|
+
description: z.string().optional().describe("New description"),
|
|
51
|
+
status_id: z.number().optional().describe("New status ID"),
|
|
52
|
+
assigned_to_id: z.number().optional().describe("New assignee user ID"),
|
|
53
|
+
notes: z.string().optional().describe("Add a comment/note to the issue"),
|
|
54
|
+
}, async (params) => {
|
|
55
|
+
const { issue_id, ...data } = params;
|
|
56
|
+
const result = await client.updateIssue(issue_id, data);
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerMembershipsTools(server, client) {
|
|
3
|
+
server.registerTool("list_project_memberships", {
|
|
4
|
+
description: "List all memberships (users and groups) for a project",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier"),
|
|
7
|
+
limit: z.number().optional().describe("Maximum results"),
|
|
8
|
+
offset: z.number().optional().describe("Skip first N results"),
|
|
9
|
+
},
|
|
10
|
+
}, async (params) => {
|
|
11
|
+
const { project_id, ...rest } = params;
|
|
12
|
+
const result = await client.listProjectMemberships(project_id, rest);
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
server.registerTool("get_membership", {
|
|
18
|
+
description: "Get details of a specific membership",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
membership_id: z.number().describe("Membership ID"),
|
|
21
|
+
},
|
|
22
|
+
}, async (params) => {
|
|
23
|
+
const result = await client.getMembership(params.membership_id);
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
server.registerTool("create_project_membership", {
|
|
29
|
+
description: "Add a user or group to a project with specified roles",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
project_id: z.union([z.string(), z.number()]).describe("Project ID or identifier"),
|
|
32
|
+
user_id: z.number().describe("User ID or Group ID to add"),
|
|
33
|
+
role_ids: z.array(z.number()).describe("Role IDs to assign (use list_roles to get IDs)"),
|
|
34
|
+
},
|
|
35
|
+
}, async (params) => {
|
|
36
|
+
const { project_id, ...data } = params;
|
|
37
|
+
const result = await client.createProjectMembership(project_id, data);
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
server.registerTool("update_membership", {
|
|
43
|
+
description: "Update roles for a membership (cannot change user/project)",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
membership_id: z.number().describe("Membership ID to update"),
|
|
46
|
+
role_ids: z.array(z.number()).describe("New role IDs"),
|
|
47
|
+
},
|
|
48
|
+
}, async (params) => {
|
|
49
|
+
const { membership_id, ...data } = params;
|
|
50
|
+
const result = await client.updateMembership(membership_id, data);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
server.registerTool("delete_membership", {
|
|
56
|
+
description: "Remove a user or group from a project",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
membership_id: z.number().describe("Membership ID to delete"),
|
|
59
|
+
},
|
|
60
|
+
}, async (params) => {
|
|
61
|
+
const result = await client.deleteMembership(params.membership_id);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { success: true }, null, 2) }],
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|