@martin4455/redmine-mcp-ro 0.3.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/server.js ADDED
@@ -0,0 +1,222 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { RedmineClient } from './redmine-client.js';
5
+ import { UserResolver } from './user-resolver.js';
6
+ import { AttachmentDownloader } from './attachment-downloader.js';
7
+ import { AttachmentParser } from './attachment-parser.js';
8
+ import { tools } from './tool-definitions.js';
9
+ import { ToolArgumentValidator } from './tool-validation.js';
10
+ import { safeErrorMessage } from './safe-error.js';
11
+ import { stripApiKeys } from './api-key-filter.js';
12
+ export async function startServer(redmineConfig) {
13
+ // Initialize Redmine client
14
+ const redmineClient = new RedmineClient(redmineConfig);
15
+ // Initialize user resolver
16
+ const userResolver = new UserResolver(redmineClient);
17
+ // Initialize attachment downloader
18
+ const attachmentDownloader = new AttachmentDownloader(redmineConfig);
19
+ // Initialize attachment parser
20
+ const attachmentParser = new AttachmentParser();
21
+ // Create MCP server
22
+ const server = new Server({
23
+ name: 'redmine-mcp-ro',
24
+ version: '0.3.0',
25
+ }, {
26
+ capabilities: {
27
+ tools: {},
28
+ },
29
+ });
30
+ const toolValidator = new ToolArgumentValidator(tools);
31
+ // Handle list tools request
32
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
33
+ return { tools };
34
+ });
35
+ // Handle tool execution
36
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
37
+ const { name, arguments: input = {} } = request.params;
38
+ try {
39
+ const args = toolValidator.validate(name, input);
40
+ let result;
41
+ switch (name) {
42
+ case 'list_issues': {
43
+ // Resolve assigned_to_id if it's a string (name or email)
44
+ const params = { ...args };
45
+ if (params.assigned_to_id) {
46
+ params.assigned_to_id = await userResolver.resolveUser(params.assigned_to_id);
47
+ }
48
+ result = await redmineClient.getIssues(params);
49
+ break;
50
+ }
51
+ case 'get_issue': {
52
+ const issueData = await redmineClient.getIssue(args.id, args.include);
53
+ // If attachments or journals are included, enhance with embedded attachments
54
+ if (args.include?.includes('attachments') || args.include?.includes('journals')) {
55
+ const embeddedAttachments = attachmentParser.findAllAttachments(issueData, redmineConfig.url);
56
+ // Add embedded_attachments field if we found any
57
+ if (embeddedAttachments.length > 0) {
58
+ // Mark which attachments are from API vs embedded
59
+ if (issueData.attachments) {
60
+ const apiIds = new Set(issueData.attachments.map((a) => a.id));
61
+ issueData.embedded_attachments = embeddedAttachments.map((att) => ({
62
+ ...att,
63
+ source: apiIds.has(att.id) ? 'api' : 'embedded'
64
+ }));
65
+ }
66
+ else {
67
+ issueData.embedded_attachments = embeddedAttachments.map((att) => ({
68
+ ...att,
69
+ source: 'embedded'
70
+ }));
71
+ }
72
+ }
73
+ }
74
+ result = issueData;
75
+ break;
76
+ }
77
+ case 'list_projects':
78
+ result = await redmineClient.getProjects(args);
79
+ break;
80
+ case 'get_project':
81
+ result = await redmineClient.getProject(args.id, args.include);
82
+ break;
83
+ case 'list_users':
84
+ result = await redmineClient.getUsers(args);
85
+ break;
86
+ case 'get_user':
87
+ result = await redmineClient.getUser(args.id, args.include);
88
+ break;
89
+ case 'get_current_user':
90
+ result = await redmineClient.getCurrentUser();
91
+ break;
92
+ case 'list_time_entries':
93
+ result = await redmineClient.getTimeEntries(args);
94
+ break;
95
+ case 'get_time_entry':
96
+ result = await redmineClient.getTimeEntry(args.id);
97
+ break;
98
+ case 'list_project_versions':
99
+ result = await redmineClient.getProjectVersions(args.project_id);
100
+ break;
101
+ case 'get_version':
102
+ result = await redmineClient.getVersion(args.id);
103
+ break;
104
+ case 'list_news':
105
+ if (args.project_id) {
106
+ result = await redmineClient.getProjectNews(args.project_id, args);
107
+ }
108
+ else {
109
+ result = await redmineClient.getNews(args);
110
+ }
111
+ break;
112
+ case 'list_wiki_pages':
113
+ result = await redmineClient.getWikiPages(args.project_id);
114
+ break;
115
+ case 'get_wiki_page':
116
+ result = await redmineClient.getWikiPage(args.project_id, args.title, args.version);
117
+ break;
118
+ case 'list_project_memberships':
119
+ result = await redmineClient.getProjectMemberships(args.project_id, args);
120
+ break;
121
+ case 'get_membership':
122
+ result = await redmineClient.getMembership(args.id);
123
+ break;
124
+ case 'list_issue_statuses':
125
+ result = await redmineClient.getIssueStatuses();
126
+ break;
127
+ case 'list_trackers':
128
+ result = await redmineClient.getTrackers();
129
+ break;
130
+ case 'list_issue_priorities':
131
+ result = await redmineClient.getIssuePriorities();
132
+ break;
133
+ case 'list_time_entry_activities':
134
+ result = await redmineClient.getTimeEntryActivities();
135
+ break;
136
+ case 'list_roles':
137
+ result = await redmineClient.getRoles();
138
+ break;
139
+ case 'get_role':
140
+ result = await redmineClient.getRole(args.id);
141
+ break;
142
+ case 'list_groups':
143
+ result = await redmineClient.getGroups(args);
144
+ break;
145
+ case 'get_group':
146
+ result = await redmineClient.getGroup(args.id, args.include);
147
+ break;
148
+ case 'list_custom_fields':
149
+ result = await redmineClient.getCustomFields();
150
+ break;
151
+ case 'search':
152
+ result = await redmineClient.search(args.query, args);
153
+ break;
154
+ case 'list_project_issue_categories':
155
+ result = await redmineClient.getProjectIssueCategories(args.project_id);
156
+ break;
157
+ case 'get_issue_category':
158
+ result = await redmineClient.getIssueCategory(args.id);
159
+ break;
160
+ case 'get_attachment':
161
+ result = await redmineClient.getAttachment(args.id);
162
+ break;
163
+ case 'list_issue_relations':
164
+ result = await redmineClient.getIssueRelations(args.issue_id);
165
+ break;
166
+ case 'get_relation':
167
+ result = await redmineClient.getRelation(args.id);
168
+ break;
169
+ case 'list_queries':
170
+ result = await redmineClient.getQueries(args);
171
+ break;
172
+ case 'list_project_files':
173
+ result = await redmineClient.getProjectFiles(args.project_id);
174
+ break;
175
+ case 'download_attachment':
176
+ result = await attachmentDownloader.downloadAttachment(args.attachment_id, args.output_dir, {
177
+ version: args.version,
178
+ timestamp: args.timestamp,
179
+ });
180
+ break;
181
+ case 'download_issue_attachments':
182
+ result = await attachmentDownloader.downloadAllIssueAttachments(args.issue_id, args.output_dir, true // Include embedded attachments
183
+ );
184
+ break;
185
+ case 'get_attachment_content':
186
+ result = await attachmentDownloader.getAttachmentContent(args.attachment_id);
187
+ break;
188
+ default:
189
+ return {
190
+ content: [
191
+ {
192
+ type: 'text',
193
+ text: `Unknown tool: ${name}`,
194
+ },
195
+ ],
196
+ };
197
+ }
198
+ return {
199
+ content: [
200
+ {
201
+ type: 'text',
202
+ text: JSON.stringify(stripApiKeys(result), null, 2),
203
+ },
204
+ ],
205
+ };
206
+ }
207
+ catch (error) {
208
+ return {
209
+ content: [
210
+ {
211
+ type: 'text',
212
+ text: `Error: ${safeErrorMessage(error, [redmineConfig.apiKey])}`,
213
+ },
214
+ ],
215
+ isError: true,
216
+ };
217
+ }
218
+ });
219
+ const transport = new StdioServerTransport();
220
+ await server.connect(transport);
221
+ console.error('Redmine MCP server started successfully');
222
+ }
@@ -0,0 +1,2 @@
1
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare const tools: Tool[];
@@ -0,0 +1,460 @@
1
+ // Define available tools
2
+ export const tools = [
3
+ {
4
+ name: 'list_issues',
5
+ description: 'List and search Redmine issues with optional filters',
6
+ inputSchema: {
7
+ type: 'object',
8
+ additionalProperties: false,
9
+ properties: {
10
+ project_id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier' },
11
+ status_id: { type: 'string', minLength: 1, maxLength: 4096, description: 'Status ID or name (open, closed, *)' },
12
+ assigned_to_id: { type: 'string', minLength: 1, maxLength: 4096, description: 'User ID, name (First Last), email, "me"/"current" for current user' },
13
+ tracker_id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Tracker ID' },
14
+ query_id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Query ID' },
15
+ offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: 'Offset for pagination' },
16
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Limit for pagination (max 100)' },
17
+ sort: { type: 'string', minLength: 1, maxLength: 4096, description: 'Sort order' },
18
+ },
19
+ },
20
+ },
21
+ {
22
+ name: 'get_issue',
23
+ description: 'Get details of a specific Redmine issue. When including "attachments" or "journals", also finds attachments embedded in HTML descriptions and comments.',
24
+ inputSchema: {
25
+ type: 'object',
26
+ additionalProperties: false,
27
+ properties: {
28
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Issue ID' },
29
+ include: {
30
+ type: 'array', maxItems: 20, uniqueItems: true,
31
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
32
+ description: 'Additional data to include (children, attachments, relations, changesets, journals, watchers). Including "attachments" or "journals" will also find embedded attachments in HTML.',
33
+ },
34
+ },
35
+ required: ['id'],
36
+ },
37
+ },
38
+ {
39
+ name: 'list_projects',
40
+ description: 'List all Redmine projects',
41
+ inputSchema: {
42
+ type: 'object',
43
+ additionalProperties: false,
44
+ properties: {
45
+ offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: 'Offset for pagination' },
46
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Limit for pagination' },
47
+ },
48
+ },
49
+ },
50
+ {
51
+ name: 'get_project',
52
+ description: 'Get details of a specific Redmine project',
53
+ inputSchema: {
54
+ type: 'object',
55
+ additionalProperties: false,
56
+ properties: {
57
+ id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier' },
58
+ include: {
59
+ type: 'array', maxItems: 20, uniqueItems: true,
60
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
61
+ description: 'Additional data to include (trackers, issue_categories, enabled_modules)',
62
+ },
63
+ },
64
+ required: ['id'],
65
+ },
66
+ },
67
+ {
68
+ name: 'list_users',
69
+ description: 'List Redmine users (requires admin permissions)',
70
+ inputSchema: {
71
+ type: 'object',
72
+ additionalProperties: false,
73
+ properties: {
74
+ offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: 'Offset for pagination' },
75
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Limit for pagination' },
76
+ },
77
+ },
78
+ },
79
+ {
80
+ name: 'get_user',
81
+ description: 'Get details of a specific Redmine user',
82
+ inputSchema: {
83
+ type: 'object',
84
+ additionalProperties: false,
85
+ properties: {
86
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'User ID' },
87
+ include: {
88
+ type: 'array', maxItems: 20, uniqueItems: true,
89
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
90
+ description: 'Additional data to include (memberships, groups)',
91
+ },
92
+ },
93
+ required: ['id'],
94
+ },
95
+ },
96
+ {
97
+ name: 'get_current_user',
98
+ description: 'Get details of the current authenticated user',
99
+ inputSchema: {
100
+ type: 'object',
101
+ additionalProperties: false,
102
+ properties: {},
103
+ },
104
+ },
105
+ {
106
+ name: 'list_time_entries',
107
+ description: 'List time entries',
108
+ inputSchema: {
109
+ type: 'object',
110
+ additionalProperties: false,
111
+ properties: {
112
+ project_id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Project ID' },
113
+ user_id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'User ID' },
114
+ offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: 'Offset for pagination' },
115
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Limit for pagination' },
116
+ },
117
+ },
118
+ },
119
+ {
120
+ name: 'get_time_entry',
121
+ description: 'Get details of a specific time entry',
122
+ inputSchema: {
123
+ type: 'object',
124
+ additionalProperties: false,
125
+ properties: {
126
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Time entry ID' },
127
+ },
128
+ required: ['id'],
129
+ },
130
+ },
131
+ {
132
+ name: 'list_project_versions',
133
+ description: 'List versions for a project',
134
+ inputSchema: {
135
+ type: 'object',
136
+ additionalProperties: false,
137
+ properties: {
138
+ project_id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier' },
139
+ },
140
+ required: ['project_id'],
141
+ },
142
+ },
143
+ {
144
+ name: 'get_version',
145
+ description: 'Get details of a specific version',
146
+ inputSchema: {
147
+ type: 'object',
148
+ additionalProperties: false,
149
+ properties: {
150
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Version ID' },
151
+ },
152
+ required: ['id'],
153
+ },
154
+ },
155
+ {
156
+ name: 'list_news',
157
+ description: 'List news items',
158
+ inputSchema: {
159
+ type: 'object',
160
+ additionalProperties: false,
161
+ properties: {
162
+ project_id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier (optional)' },
163
+ offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: 'Offset for pagination' },
164
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Limit for pagination' },
165
+ },
166
+ },
167
+ },
168
+ {
169
+ name: 'list_wiki_pages',
170
+ description: 'List wiki pages for a project',
171
+ inputSchema: {
172
+ type: 'object',
173
+ additionalProperties: false,
174
+ properties: {
175
+ project_id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier' },
176
+ },
177
+ required: ['project_id'],
178
+ },
179
+ },
180
+ {
181
+ name: 'get_wiki_page',
182
+ description: 'Get content of a specific wiki page',
183
+ inputSchema: {
184
+ type: 'object',
185
+ additionalProperties: false,
186
+ properties: {
187
+ project_id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier' },
188
+ title: { type: 'string', minLength: 1, maxLength: 4096, description: 'Wiki page title' },
189
+ version: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Page version (optional)' },
190
+ },
191
+ required: ['project_id', 'title'],
192
+ },
193
+ },
194
+ {
195
+ name: 'list_project_memberships',
196
+ description: 'List memberships for a project',
197
+ inputSchema: {
198
+ type: 'object',
199
+ additionalProperties: false,
200
+ properties: {
201
+ project_id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier' },
202
+ offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: 'Offset for pagination' },
203
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Limit for pagination' },
204
+ },
205
+ required: ['project_id'],
206
+ },
207
+ },
208
+ {
209
+ name: 'get_membership',
210
+ description: 'Get details of a specific membership',
211
+ inputSchema: {
212
+ type: 'object',
213
+ additionalProperties: false,
214
+ properties: {
215
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Membership ID' },
216
+ },
217
+ required: ['id'],
218
+ },
219
+ },
220
+ {
221
+ name: 'list_issue_statuses',
222
+ description: 'List all available issue statuses',
223
+ inputSchema: {
224
+ type: 'object',
225
+ additionalProperties: false,
226
+ properties: {},
227
+ },
228
+ },
229
+ {
230
+ name: 'list_trackers',
231
+ description: 'List all available issue trackers',
232
+ inputSchema: {
233
+ type: 'object',
234
+ additionalProperties: false,
235
+ properties: {},
236
+ },
237
+ },
238
+ {
239
+ name: 'list_issue_priorities',
240
+ description: 'List all available issue priorities',
241
+ inputSchema: {
242
+ type: 'object',
243
+ additionalProperties: false,
244
+ properties: {},
245
+ },
246
+ },
247
+ {
248
+ name: 'list_time_entry_activities',
249
+ description: 'List all available time entry activities',
250
+ inputSchema: {
251
+ type: 'object',
252
+ additionalProperties: false,
253
+ properties: {},
254
+ },
255
+ },
256
+ {
257
+ name: 'list_roles',
258
+ description: 'List all available roles',
259
+ inputSchema: {
260
+ type: 'object',
261
+ additionalProperties: false,
262
+ properties: {},
263
+ },
264
+ },
265
+ {
266
+ name: 'get_role',
267
+ description: 'Get details of a specific role',
268
+ inputSchema: {
269
+ type: 'object',
270
+ additionalProperties: false,
271
+ properties: {
272
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Role ID' },
273
+ },
274
+ required: ['id'],
275
+ },
276
+ },
277
+ {
278
+ name: 'list_groups',
279
+ description: 'List all groups (requires admin permissions)',
280
+ inputSchema: {
281
+ type: 'object',
282
+ additionalProperties: false,
283
+ properties: {
284
+ offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: 'Offset for pagination' },
285
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Limit for pagination' },
286
+ },
287
+ },
288
+ },
289
+ {
290
+ name: 'get_group',
291
+ description: 'Get details of a specific group',
292
+ inputSchema: {
293
+ type: 'object',
294
+ additionalProperties: false,
295
+ properties: {
296
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Group ID' },
297
+ include: {
298
+ type: 'array', maxItems: 20, uniqueItems: true,
299
+ items: { type: 'string', minLength: 1, maxLength: 4096 },
300
+ description: 'Additional data to include (users, memberships)',
301
+ },
302
+ },
303
+ required: ['id'],
304
+ },
305
+ },
306
+ {
307
+ name: 'list_custom_fields',
308
+ description: 'List all custom field definitions',
309
+ inputSchema: {
310
+ type: 'object',
311
+ additionalProperties: false,
312
+ properties: {},
313
+ },
314
+ },
315
+ {
316
+ name: 'search',
317
+ description: 'Search across Redmine resources',
318
+ inputSchema: {
319
+ type: 'object',
320
+ additionalProperties: false,
321
+ properties: {
322
+ query: { type: 'string', minLength: 1, maxLength: 4096, description: 'Search query' },
323
+ issues: { type: 'boolean', description: 'Search in issues' },
324
+ news: { type: 'boolean', description: 'Search in news' },
325
+ documents: { type: 'boolean', description: 'Search in documents' },
326
+ changesets: { type: 'boolean', description: 'Search in changesets' },
327
+ wiki_pages: { type: 'boolean', description: 'Search in wiki pages' },
328
+ messages: { type: 'boolean', description: 'Search in messages' },
329
+ projects: { type: 'boolean', description: 'Search in projects' },
330
+ },
331
+ required: ['query'],
332
+ },
333
+ },
334
+ {
335
+ name: 'list_project_issue_categories',
336
+ description: 'List issue categories for a project',
337
+ inputSchema: {
338
+ type: 'object',
339
+ additionalProperties: false,
340
+ properties: {
341
+ project_id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier' },
342
+ },
343
+ required: ['project_id'],
344
+ },
345
+ },
346
+ {
347
+ name: 'get_issue_category',
348
+ description: 'Get details of a specific issue category',
349
+ inputSchema: {
350
+ type: 'object',
351
+ additionalProperties: false,
352
+ properties: {
353
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Issue category ID' },
354
+ },
355
+ required: ['id'],
356
+ },
357
+ },
358
+ {
359
+ name: 'get_attachment',
360
+ description: 'Get details of a specific attachment',
361
+ inputSchema: {
362
+ type: 'object',
363
+ additionalProperties: false,
364
+ properties: {
365
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Attachment ID' },
366
+ },
367
+ required: ['id'],
368
+ },
369
+ },
370
+ {
371
+ name: 'list_issue_relations',
372
+ description: 'List relations for an issue',
373
+ inputSchema: {
374
+ type: 'object',
375
+ additionalProperties: false,
376
+ properties: {
377
+ issue_id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Issue ID' },
378
+ },
379
+ required: ['issue_id'],
380
+ },
381
+ },
382
+ {
383
+ name: 'get_relation',
384
+ description: 'Get details of a specific issue relation',
385
+ inputSchema: {
386
+ type: 'object',
387
+ additionalProperties: false,
388
+ properties: {
389
+ id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Relation ID' },
390
+ },
391
+ required: ['id'],
392
+ },
393
+ },
394
+ {
395
+ name: 'list_queries',
396
+ description: 'List saved queries',
397
+ inputSchema: {
398
+ type: 'object',
399
+ additionalProperties: false,
400
+ properties: {
401
+ offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: 'Offset for pagination' },
402
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Limit for pagination' },
403
+ },
404
+ },
405
+ },
406
+ {
407
+ name: 'list_project_files',
408
+ description: 'List files for a project',
409
+ inputSchema: {
410
+ type: 'object',
411
+ additionalProperties: false,
412
+ properties: {
413
+ project_id: { type: 'string', minLength: 1, maxLength: 4096, pattern: '^[a-zA-Z0-9_-]+$', description: 'Project ID or identifier' },
414
+ },
415
+ required: ['project_id'],
416
+ },
417
+ },
418
+ {
419
+ name: 'download_attachment',
420
+ description: 'Save an attachment inside the operator-configured download directory. Disabled until a download directory is configured.',
421
+ annotations: { readOnlyHint: false, destructiveHint: false },
422
+ inputSchema: {
423
+ type: 'object',
424
+ additionalProperties: false,
425
+ properties: {
426
+ attachment_id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Attachment ID' },
427
+ output_dir: { type: 'string', minLength: 1, maxLength: 4096, description: 'Optional relative subdirectory of the configured download directory; defaults to its root. Absolute paths, parent traversal and symlinks are rejected.' },
428
+ version: { type: 'boolean', description: 'Include version parameter (for Easy Redmine)' },
429
+ timestamp: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Timestamp parameter (for Easy Redmine)' },
430
+ },
431
+ required: ['attachment_id'],
432
+ },
433
+ },
434
+ {
435
+ name: 'download_issue_attachments',
436
+ description: 'Save issue attachments, including embedded attachments, inside the operator-configured download directory. Disabled until a download directory is configured.',
437
+ annotations: { readOnlyHint: false, destructiveHint: false },
438
+ inputSchema: {
439
+ type: 'object',
440
+ additionalProperties: false,
441
+ properties: {
442
+ issue_id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Issue ID' },
443
+ output_dir: { type: 'string', minLength: 1, maxLength: 4096, description: 'Optional relative subdirectory of the configured download directory; defaults to its root. Absolute paths, parent traversal and symlinks are rejected.' },
444
+ },
445
+ required: ['issue_id'],
446
+ },
447
+ },
448
+ {
449
+ name: 'get_attachment_content',
450
+ description: 'Get attachment content as base64 (for smaller files)',
451
+ inputSchema: {
452
+ type: 'object',
453
+ additionalProperties: false,
454
+ properties: {
455
+ attachment_id: { type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER, description: 'Attachment ID' },
456
+ },
457
+ required: ['attachment_id'],
458
+ },
459
+ },
460
+ ];