@muzzamil-khan/redmine-agent-mcp 1.0.0 → 1.1.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.
Files changed (111) hide show
  1. package/dist/config/env.d.ts +2 -0
  2. package/dist/config/env.d.ts.map +1 -1
  3. package/dist/config/env.js +8 -0
  4. package/dist/config/env.js.map +1 -1
  5. package/dist/mcp/server.js +630 -147
  6. package/dist/mcp/server.js.map +1 -1
  7. package/dist/services/assigneeDirectory.d.ts +34 -0
  8. package/dist/services/assigneeDirectory.d.ts.map +1 -0
  9. package/dist/services/assigneeDirectory.js +138 -0
  10. package/dist/services/assigneeDirectory.js.map +1 -0
  11. package/dist/services/googleSheets.d.ts +32 -0
  12. package/dist/services/googleSheets.d.ts.map +1 -0
  13. package/dist/services/googleSheets.js +109 -0
  14. package/dist/services/googleSheets.js.map +1 -0
  15. package/dist/services/redmine.d.ts +52 -0
  16. package/dist/services/redmine.d.ts.map +1 -1
  17. package/dist/services/redmine.js +103 -0
  18. package/dist/services/redmine.js.map +1 -1
  19. package/dist/services/redmineConfig.d.ts +85 -0
  20. package/dist/services/redmineConfig.d.ts.map +1 -0
  21. package/dist/services/redmineConfig.js +271 -0
  22. package/dist/services/redmineConfig.js.map +1 -0
  23. package/dist/spreadsheet/batchColumnMapping.d.ts +93 -0
  24. package/dist/spreadsheet/batchColumnMapping.d.ts.map +1 -0
  25. package/dist/spreadsheet/batchColumnMapping.js +660 -0
  26. package/dist/spreadsheet/batchColumnMapping.js.map +1 -0
  27. package/dist/spreadsheet/batchCreationTypes.d.ts +58 -0
  28. package/dist/spreadsheet/batchCreationTypes.d.ts.map +1 -0
  29. package/dist/spreadsheet/batchCreationTypes.js +18 -0
  30. package/dist/spreadsheet/batchCreationTypes.js.map +1 -0
  31. package/dist/spreadsheet/batchIssueConfig.d.ts +21 -0
  32. package/dist/spreadsheet/batchIssueConfig.d.ts.map +1 -0
  33. package/dist/spreadsheet/batchIssueConfig.js +73 -0
  34. package/dist/spreadsheet/batchIssueConfig.js.map +1 -0
  35. package/dist/spreadsheet/batchPreviewFormat.d.ts +39 -0
  36. package/dist/spreadsheet/batchPreviewFormat.d.ts.map +1 -0
  37. package/dist/spreadsheet/batchPreviewFormat.js +190 -0
  38. package/dist/spreadsheet/batchPreviewFormat.js.map +1 -0
  39. package/dist/spreadsheet/batchSheetHeuristics.d.ts +13 -0
  40. package/dist/spreadsheet/batchSheetHeuristics.d.ts.map +1 -0
  41. package/dist/spreadsheet/batchSheetHeuristics.js +30 -0
  42. package/dist/spreadsheet/batchSheetHeuristics.js.map +1 -0
  43. package/dist/spreadsheet/csvUtils.d.ts +14 -0
  44. package/dist/spreadsheet/csvUtils.d.ts.map +1 -0
  45. package/dist/spreadsheet/csvUtils.js +113 -0
  46. package/dist/spreadsheet/csvUtils.js.map +1 -0
  47. package/dist/spreadsheet/extractRawBatchSheet.d.ts +14 -0
  48. package/dist/spreadsheet/extractRawBatchSheet.d.ts.map +1 -0
  49. package/dist/spreadsheet/extractRawBatchSheet.js +33 -0
  50. package/dist/spreadsheet/extractRawBatchSheet.js.map +1 -0
  51. package/dist/spreadsheet/featureRow.d.ts +15 -0
  52. package/dist/spreadsheet/featureRow.d.ts.map +1 -0
  53. package/dist/spreadsheet/featureRow.js +119 -0
  54. package/dist/spreadsheet/featureRow.js.map +1 -0
  55. package/dist/spreadsheet/parseBatchCreationCsv.d.ts +8 -0
  56. package/dist/spreadsheet/parseBatchCreationCsv.d.ts.map +1 -0
  57. package/dist/spreadsheet/parseBatchCreationCsv.js +22 -0
  58. package/dist/spreadsheet/parseBatchCreationCsv.js.map +1 -0
  59. package/dist/spreadsheet/parseBatchCreationCsvV1.d.ts +3 -0
  60. package/dist/spreadsheet/parseBatchCreationCsvV1.d.ts.map +1 -0
  61. package/dist/spreadsheet/parseBatchCreationCsvV1.js +226 -0
  62. package/dist/spreadsheet/parseBatchCreationCsvV1.js.map +1 -0
  63. package/dist/spreadsheet/parseBatchCreationCsvV2.d.ts +4 -0
  64. package/dist/spreadsheet/parseBatchCreationCsvV2.d.ts.map +1 -0
  65. package/dist/spreadsheet/parseBatchCreationCsvV2.js +180 -0
  66. package/dist/spreadsheet/parseBatchCreationCsvV2.js.map +1 -0
  67. package/dist/spreadsheet/redmineIssueCell.d.ts +38 -0
  68. package/dist/spreadsheet/redmineIssueCell.d.ts.map +1 -0
  69. package/dist/spreadsheet/redmineIssueCell.js +176 -0
  70. package/dist/spreadsheet/redmineIssueCell.js.map +1 -0
  71. package/dist/spreadsheet/sheetColumnMap.d.ts +38 -0
  72. package/dist/spreadsheet/sheetColumnMap.d.ts.map +1 -0
  73. package/dist/spreadsheet/sheetColumnMap.js +75 -0
  74. package/dist/spreadsheet/sheetColumnMap.js.map +1 -0
  75. package/dist/spreadsheet/sheetGridToCsv.d.ts +28 -0
  76. package/dist/spreadsheet/sheetGridToCsv.d.ts.map +1 -0
  77. package/dist/spreadsheet/sheetGridToCsv.js +88 -0
  78. package/dist/spreadsheet/sheetGridToCsv.js.map +1 -0
  79. package/dist/spreadsheet/sheetRedmineFields.d.ts +7 -0
  80. package/dist/spreadsheet/sheetRedmineFields.d.ts.map +1 -0
  81. package/dist/spreadsheet/sheetRedmineFields.js +22 -0
  82. package/dist/spreadsheet/sheetRedmineFields.js.map +1 -0
  83. package/dist/spreadsheet/sheetWriteBack.d.ts +27 -0
  84. package/dist/spreadsheet/sheetWriteBack.d.ts.map +1 -0
  85. package/dist/spreadsheet/sheetWriteBack.js +90 -0
  86. package/dist/spreadsheet/sheetWriteBack.js.map +1 -0
  87. package/dist/workflows/batchCreateIssuesWorkflow.d.ts +66 -0
  88. package/dist/workflows/batchCreateIssuesWorkflow.d.ts.map +1 -0
  89. package/dist/workflows/batchCreateIssuesWorkflow.js +1014 -0
  90. package/dist/workflows/batchCreateIssuesWorkflow.js.map +1 -0
  91. package/dist/workflows/createIssueWorkflow.d.ts +41 -0
  92. package/dist/workflows/createIssueWorkflow.d.ts.map +1 -0
  93. package/dist/workflows/createIssueWorkflow.js +697 -0
  94. package/dist/workflows/createIssueWorkflow.js.map +1 -0
  95. package/dist/workflows/getAllUsersWorkflow.d.ts +27 -0
  96. package/dist/workflows/getAllUsersWorkflow.d.ts.map +1 -0
  97. package/dist/workflows/getAllUsersWorkflow.js +48 -0
  98. package/dist/workflows/getAllUsersWorkflow.js.map +1 -0
  99. package/dist/workflows/issueCreationHelpers.d.ts +54 -0
  100. package/dist/workflows/issueCreationHelpers.d.ts.map +1 -0
  101. package/dist/workflows/issueCreationHelpers.js +272 -0
  102. package/dist/workflows/issueCreationHelpers.js.map +1 -0
  103. package/dist/workflows/logTimeWorkflow.d.ts +6 -1
  104. package/dist/workflows/logTimeWorkflow.d.ts.map +1 -1
  105. package/dist/workflows/logTimeWorkflow.js +139 -20
  106. package/dist/workflows/logTimeWorkflow.js.map +1 -1
  107. package/dist/workflows/sheetToCsvWorkflow.d.ts +15 -0
  108. package/dist/workflows/sheetToCsvWorkflow.d.ts.map +1 -0
  109. package/dist/workflows/sheetToCsvWorkflow.js +65 -0
  110. package/dist/workflows/sheetToCsvWorkflow.js.map +1 -0
  111. package/package.json +2 -2
@@ -3,155 +3,638 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import * as z from "zod/v4";
5
5
  import { logTimeWorkflow } from "../workflows/logTimeWorkflow.js";
6
+ import { createIssueWorkflow } from "../workflows/createIssueWorkflow.js";
7
+ import { batchCreateIssuesWorkflow } from "../workflows/batchCreateIssuesWorkflow.js";
8
+ import { sheetToCsvWorkflow } from "../workflows/sheetToCsvWorkflow.js";
9
+ import { getAllUsersWorkflow } from "../workflows/getAllUsersWorkflow.js";
6
10
  import { workflowToMcpContent } from "../utils/mcpResponse.js";
7
- const server = new McpServer({
8
- name: "redmine-agent",
9
- version: "3.4.1",
10
- }, {
11
- instructions: `Local Redmine tool: batch log time only (redmine_agent_log_time).
12
- When needsInput and data.batchMode is true, show ALL items in questions[] in ONE AskQuestion. dateIssues ids are YYYY-MM-DD; pairHours/pairComments ids are YYYY-MM-DD:issueId.
13
- Flow: dates batch dateIssues batch pairHours batch pairComments preview confirmSave=yes (no yes/no pair step).`,
14
- });
15
- server.registerTool("redmine_agent_log_time", {
16
- description: "Batch log time: dates → batch dateIssues (per date, one page) → batch pairHours → batch pairComments → preview → confirmSave=yes. Selecting issues per date defines what to log; no pairInclude step.",
17
- inputSchema: {
18
- promptHint: z
19
- .string()
20
- .optional()
21
- .describe("User message text; if it mentions 'today', date step is skipped"),
22
- dates: z
23
- .array(z.string())
24
- .optional()
25
- .describe("Step 1 Today | Multiple | Other, or YYYY-MM-DD list"),
26
- spentOn: z
27
- .string()
28
- .optional()
29
- .describe("Custom date YYYY-MM-DD when Other was chosen"),
30
- pendingDates: z.array(z.string()).optional(),
31
- activeDate: z.string().optional(),
32
- dateIssues: z
33
- .record(z.string(), z.union([
34
- z.array(z.number().int().positive()),
35
- z.string(),
36
- ]))
37
- .optional()
38
- .describe("Step 2 batch — issue IDs per date (number[] or comma-separated string)"),
39
- pendingIssueIds: z.array(z.number().int().positive()).optional(),
40
- activeIssueId: z.number().int().positive().optional(),
41
- issueIds: z
42
- .array(z.number().int().positive())
43
- .optional()
44
- .describe("Legacy — fills all dates if dateIssues omitted (single date only)"),
45
- pairInclude: z
46
- .enum(["yes", "no"])
47
- .optional()
48
- .describe("Legacy single pair; prefer pairIncludes"),
49
- pairIncludes: z
50
- .record(z.string(), z.string())
51
- .optional()
52
- .describe('Step 3 batch — all yes/no keyed by "YYYY-MM-DD:issueId"'),
53
- pairHours: z
54
- .record(z.string(), z.union([z.number(), z.string()]))
55
- .optional()
56
- .describe("Step 4 batch — hours per included pair key"),
57
- pairComments: z
58
- .record(z.string(), z.string())
59
- .optional()
60
- .describe('Step 5 batch — "default" or custom text per pair key'),
61
- issueSubjects: z
62
- .record(z.string(), z.string())
63
- .optional()
64
- .describe("issueId subject; pass back from data for question labels"),
65
- issueOptions: z
66
- .array(z.object({
67
- issueId: z.number().int().positive(),
68
- subject: z.string().optional(),
69
- label: z.string().optional(),
70
- }))
71
- .optional(),
72
- skippedPairs: z
73
- .array(z.string())
74
- .optional()
75
- .describe('Skipped pair keys "YYYY-MM-DD:issueId"'),
76
- issueId: z.number().int().positive().optional(),
77
- hours: z.number().positive().optional().describe("Legacy single pair hours"),
78
- commentChoice: z.enum(["default", "custom"]).optional(),
79
- comments: z.string().optional(),
80
- entries: z
81
- .array(z.object({
82
- issueId: z.number().int().positive(),
83
- hours: z.number().positive(),
84
- spentOn: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
85
- comments: z.string(),
86
- }))
87
- .optional(),
88
- confirmSave: z.enum(["yes", "no"]).optional(),
89
- },
90
- }, async (input) => workflowToMcpContent(await logTimeWorkflow({
91
- ...(input.promptHint !== undefined
92
- ? { promptHint: input.promptHint }
93
- : {}),
94
- ...(input.dates !== undefined ? { dates: input.dates } : {}),
95
- ...(input.spentOn !== undefined
96
- ? { spentOn: input.spentOn }
97
- : {}),
98
- ...(input.pendingDates !== undefined
99
- ? { pendingDates: input.pendingDates }
100
- : {}),
101
- ...(input.activeDate !== undefined
102
- ? { activeDate: input.activeDate }
103
- : {}),
104
- ...(input.dateIssues !== undefined
105
- ? { dateIssues: input.dateIssues }
106
- : {}),
107
- ...(input.pendingIssueIds !== undefined
108
- ? { pendingIssueIds: input.pendingIssueIds }
109
- : {}),
110
- ...(input.activeIssueId !== undefined
111
- ? { activeIssueId: input.activeIssueId }
112
- : {}),
113
- ...(input.issueIds !== undefined
114
- ? { issueIds: input.issueIds }
115
- : {}),
116
- ...(input.issueId !== undefined
117
- ? { issueId: input.issueId }
118
- : {}),
119
- ...(input.hours !== undefined ? { hours: input.hours } : {}),
120
- ...(input.pairInclude !== undefined
121
- ? { pairInclude: input.pairInclude }
122
- : {}),
123
- ...(input.pairIncludes !== undefined
124
- ? { pairIncludes: input.pairIncludes }
125
- : {}),
126
- ...(input.pairHours !== undefined
127
- ? { pairHours: input.pairHours }
128
- : {}),
129
- ...(input.pairComments !== undefined
130
- ? { pairComments: input.pairComments }
131
- : {}),
132
- ...(input.issueSubjects !== undefined
133
- ? { issueSubjects: input.issueSubjects }
134
- : {}),
135
- ...(input.issueOptions !== undefined
136
- ? { issueOptions: input.issueOptions }
137
- : {}),
138
- ...(input.skippedPairs !== undefined
139
- ? { skippedPairs: input.skippedPairs }
140
- : {}),
141
- ...(input.commentChoice !== undefined
142
- ? { commentChoice: input.commentChoice }
143
- : {}),
144
- ...(input.comments !== undefined
145
- ? { comments: input.comments }
146
- : {}),
147
- ...(input.entries !== undefined
148
- ? { entries: input.entries }
149
- : {}),
150
- ...(input.confirmSave !== undefined
151
- ? { confirmSave: input.confirmSave }
152
- : {}),
153
- })));
11
+ import { getCurrentUserWithMemberships, getRolePermissions, } from "../services/redmine.js";
12
+ import { configOptionsPayload, formatConfigForInstructions, formatEnumerationLine, loadRedmineConfig, } from "../services/redmineConfig.js";
13
+ const BASE_MCP_INSTRUCTIONS = `Local Redmine tools: redmine_agent_log_time, redmine_agent_create_issue, redmine_agent_sheet_to_csv, redmine_agent_batch_create_issues, redmine_agent_get_all_users, redmine_agent_get_config.
14
+ Log time: when needsInput and data.batchMode is true, show ALL items in questions[] in ONE AskQuestion. Flow: dates → dateIssues → pairHours → pairActivities (mandatory; options from server config cache, not a separate MCP call) → pairComments → confirmSave=yes.
15
+ Create issue: when needsInput and data.batchMode is true, show ALL questions[] in ONE AskQuestion. Flow: full intake preview → confirmSave=yes. Use tracker/status/priority names or ids from loaded config; tracker aliases CR→Change Request, bug→Bug, story→User Story.
16
+ Sheet batch: get_sheet_data sheet_to_csv redmine_agent_get_config batch_create_issues. Steps: pick_project pick_target_version (always AskQuestion) map_columns (use suggestedColumnMapping) → confirm_mapping → fix_batch_gaps (priorityNameFixes/trackerNameFixes from config) → fix_assignees → preview_gate → proceed. Never upload on first batch call.
17
+ Get users: redmine_agent_get_all_users lists members from projects you belong to (project memberships), not global /users. Use assigneeOptions for spreadsheet Assigned To matching.
18
+ Get config: redmine_agent_get_config returns live trackers, activities, priorities, statuses (same cache as MCP connect).
19
+ Do NOT use team redmine create_issue for full task creation — use redmine_agent_create_issue or redmine_agent_batch_create_issues.`;
20
+ function enumerationDescribe(label, rows, aliasHint) {
21
+ const line = formatEnumerationLine(label, rows).replace(`${label}: `, "");
22
+ return `${label} name or id. Options: ${line}${aliasHint ? `. ${aliasHint}` : ""}`;
23
+ }
24
+ function registerTools(server, config) {
25
+ const trackerDescribe = `${enumerationDescribe("Tracker", config.trackers, "Aliases: CR→Change Request, bug→Bug, story→User Story")}`;
26
+ const statusDescribe = enumerationDescribe("Status", config.statuses);
27
+ const priorityDescribe = `${enumerationDescribe("Priority", config.priorities, "Aliases: medium→Normal, highest→Immediate, lowest→Low")}`;
28
+ server.registerTool("redmine_agent_get_config", {
29
+ description: "Redmine enumerations loaded at MCP connect (activities, trackers, priorities, statuses) with id↔name mapping and aliases. Optional force=true refreshes cache.",
30
+ inputSchema: {
31
+ force: z
32
+ .boolean()
33
+ .optional()
34
+ .describe("Bypass TTL and refetch from Redmine API"),
35
+ },
36
+ }, async (input) => {
37
+ const fresh = await loadRedmineConfig({
38
+ ...(input.force ? { force: true } : {}),
39
+ });
40
+ return workflowToMcpContent({
41
+ success: true,
42
+ data: configOptionsPayload(fresh),
43
+ });
44
+ });
45
+ server.registerTool("redmine_agent_create_issue", {
46
+ description: "Create a Redmine issue end-to-end: batched intake (all fields), preview, confirmSave=yes upload. Supports tracker, status, priority, dates, estimate, assignee, target version, parent task, and custom fields. Use this instead of team redmine create_issue.",
47
+ inputSchema: {
48
+ project: z
49
+ .string()
50
+ .optional()
51
+ .describe("Redmine project identifier or numeric project ID"),
52
+ tracker: z
53
+ .string()
54
+ .optional()
55
+ .describe(trackerDescribe),
56
+ subjectMode: z.enum(["default", "blank", "custom"]).optional(),
57
+ subjectCustom: z.string().optional(),
58
+ descriptionMode: z.enum(["default", "blank", "custom"]).optional(),
59
+ descriptionCustom: z.string().optional(),
60
+ status: z
61
+ .string()
62
+ .optional()
63
+ .describe(statusDescribe),
64
+ priority: z
65
+ .string()
66
+ .optional()
67
+ .describe(priorityDescribe),
68
+ assignedToMode: z.enum(["default", "blank", "custom"]).optional(),
69
+ assignedToCustom: z.string().optional(),
70
+ targetVersionMode: z.enum(["default", "blank", "custom"]).optional(),
71
+ targetVersionCustom: z.string().optional(),
72
+ parentTaskMode: z.enum(["default", "blank", "custom"]).optional(),
73
+ parentTaskCustom: z.string().optional(),
74
+ startDateMode: z.enum(["default", "blank", "custom"]).optional(),
75
+ startDateCustom: z.string().optional(),
76
+ dueDateMode: z.enum(["default", "blank", "custom"]).optional(),
77
+ dueDateCustom: z.string().optional(),
78
+ estimatedTimeMode: z.enum(["default", "blank", "custom"]).optional(),
79
+ estimatedTimeCustom: z.string().optional(),
80
+ qaStatusMode: z.enum(["default", "blank", "custom"]).optional(),
81
+ qaStatusCustom: z.string().optional(),
82
+ gitLinkMode: z.enum(["default", "blank", "custom"]).optional(),
83
+ gitLinkCustom: z.string().optional(),
84
+ gitLink1Mode: z.enum(["default", "blank", "custom"]).optional(),
85
+ gitLink1Custom: z.string().optional(),
86
+ gitLink2Mode: z.enum(["default", "blank", "custom"]).optional(),
87
+ gitLink2Custom: z.string().optional(),
88
+ expectedTimeMode: z.enum(["default", "blank", "custom"]).optional(),
89
+ expectedTimeCustom: z.string().optional(),
90
+ prLinkMode: z.enum(["default", "blank", "custom"]).optional(),
91
+ prLinkCustom: z.string().optional(),
92
+ storyPointsMode: z.enum(["default", "blank", "custom"]).optional(),
93
+ storyPointsCustom: z.string().optional(),
94
+ confirmSave: z.enum(["yes", "no"]).optional(),
95
+ },
96
+ }, async (input) => workflowToMcpContent(await createIssueWorkflow({
97
+ ...(input.project !== undefined ? { project: input.project } : {}),
98
+ ...(input.tracker !== undefined ? { tracker: input.tracker } : {}),
99
+ ...(input.subjectMode !== undefined
100
+ ? { subjectMode: input.subjectMode }
101
+ : {}),
102
+ ...(input.subjectCustom !== undefined
103
+ ? { subjectCustom: input.subjectCustom }
104
+ : {}),
105
+ ...(input.descriptionMode !== undefined
106
+ ? { descriptionMode: input.descriptionMode }
107
+ : {}),
108
+ ...(input.descriptionCustom !== undefined
109
+ ? { descriptionCustom: input.descriptionCustom }
110
+ : {}),
111
+ ...(input.status !== undefined ? { status: input.status } : {}),
112
+ ...(input.priority !== undefined
113
+ ? { priority: input.priority }
114
+ : {}),
115
+ ...(input.assignedToMode !== undefined
116
+ ? { assignedToMode: input.assignedToMode }
117
+ : {}),
118
+ ...(input.assignedToCustom !== undefined
119
+ ? { assignedToCustom: input.assignedToCustom }
120
+ : {}),
121
+ ...(input.targetVersionMode !== undefined
122
+ ? { targetVersionMode: input.targetVersionMode }
123
+ : {}),
124
+ ...(input.targetVersionCustom !== undefined
125
+ ? { targetVersionCustom: input.targetVersionCustom }
126
+ : {}),
127
+ ...(input.parentTaskMode !== undefined
128
+ ? { parentTaskMode: input.parentTaskMode }
129
+ : {}),
130
+ ...(input.parentTaskCustom !== undefined
131
+ ? { parentTaskCustom: input.parentTaskCustom }
132
+ : {}),
133
+ ...(input.startDateMode !== undefined
134
+ ? { startDateMode: input.startDateMode }
135
+ : {}),
136
+ ...(input.startDateCustom !== undefined
137
+ ? { startDateCustom: input.startDateCustom }
138
+ : {}),
139
+ ...(input.dueDateMode !== undefined
140
+ ? { dueDateMode: input.dueDateMode }
141
+ : {}),
142
+ ...(input.dueDateCustom !== undefined
143
+ ? { dueDateCustom: input.dueDateCustom }
144
+ : {}),
145
+ ...(input.estimatedTimeMode !== undefined
146
+ ? { estimatedTimeMode: input.estimatedTimeMode }
147
+ : {}),
148
+ ...(input.estimatedTimeCustom !== undefined
149
+ ? { estimatedTimeCustom: input.estimatedTimeCustom }
150
+ : {}),
151
+ ...(input.qaStatusMode !== undefined
152
+ ? { qaStatusMode: input.qaStatusMode }
153
+ : {}),
154
+ ...(input.qaStatusCustom !== undefined
155
+ ? { qaStatusCustom: input.qaStatusCustom }
156
+ : {}),
157
+ ...(input.gitLinkMode !== undefined
158
+ ? { gitLinkMode: input.gitLinkMode }
159
+ : {}),
160
+ ...(input.gitLinkCustom !== undefined
161
+ ? { gitLinkCustom: input.gitLinkCustom }
162
+ : {}),
163
+ ...(input.gitLink1Mode !== undefined
164
+ ? { gitLink1Mode: input.gitLink1Mode }
165
+ : {}),
166
+ ...(input.gitLink1Custom !== undefined
167
+ ? { gitLink1Custom: input.gitLink1Custom }
168
+ : {}),
169
+ ...(input.gitLink2Mode !== undefined
170
+ ? { gitLink2Mode: input.gitLink2Mode }
171
+ : {}),
172
+ ...(input.gitLink2Custom !== undefined
173
+ ? { gitLink2Custom: input.gitLink2Custom }
174
+ : {}),
175
+ ...(input.expectedTimeMode !== undefined
176
+ ? { expectedTimeMode: input.expectedTimeMode }
177
+ : {}),
178
+ ...(input.expectedTimeCustom !== undefined
179
+ ? { expectedTimeCustom: input.expectedTimeCustom }
180
+ : {}),
181
+ ...(input.prLinkMode !== undefined
182
+ ? { prLinkMode: input.prLinkMode }
183
+ : {}),
184
+ ...(input.prLinkCustom !== undefined
185
+ ? { prLinkCustom: input.prLinkCustom }
186
+ : {}),
187
+ ...(input.storyPointsMode !== undefined
188
+ ? { storyPointsMode: input.storyPointsMode }
189
+ : {}),
190
+ ...(input.storyPointsCustom !== undefined
191
+ ? { storyPointsCustom: input.storyPointsCustom }
192
+ : {}),
193
+ ...(input.confirmSave !== undefined
194
+ ? { confirmSave: input.confirmSave }
195
+ : {}),
196
+ })));
197
+ server.registerTool("redmine_agent_get_all_users", {
198
+ description: "List assignable users from projects the API user belongs to (via project memberships). Use assigneeOptions to match spreadsheet Assigned To before batch create. Optional projectId limits to one project. Returns source, projects[], users (id, name, label, roles, projectIds), assigneeOptions.",
199
+ inputSchema: {
200
+ projectId: z
201
+ .number()
202
+ .int()
203
+ .optional()
204
+ .describe("Limit to members of this project (must be in your memberships)"),
205
+ status: z
206
+ .enum(["active", "all"])
207
+ .optional()
208
+ .describe("Legacy field; membership listing does not use global /users."),
209
+ },
210
+ }, async (input) => workflowToMcpContent(await getAllUsersWorkflow({
211
+ ...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
212
+ ...(input.status !== undefined ? { status: input.status } : {}),
213
+ })));
214
+ server.registerTool("redmine_agent_sheet_to_csv", {
215
+ description: "Convert Google Sheets MCP get_sheet_data output to csvContent for batch create. Pass sheetsPayload from get_sheet_data (or rows[][]). Returns csvContent, optional csvPath cache, sheetSource (for agent write-back via Google Sheets MCP), and header preview. Does not call Google APIs — fetch the sheet via Google Sheets MCP first.",
216
+ inputSchema: {
217
+ sheetsPayload: z
218
+ .unknown()
219
+ .optional()
220
+ .describe("Full get_sheet_data result (object or JSON string)"),
221
+ rows: z
222
+ .array(z.array(z.string()))
223
+ .optional()
224
+ .describe("Raw grid when not passing sheetsPayload"),
225
+ spreadsheetId: z
226
+ .string()
227
+ .optional()
228
+ .describe("Spreadsheet id (override or required with rows-only)"),
229
+ sheet: z
230
+ .string()
231
+ .optional()
232
+ .describe("Tab name (override or from sheetsPayload range)"),
233
+ writeCache: z
234
+ .boolean()
235
+ .optional()
236
+ .describe("Write .cursor/cache/last-sheet.csv (default true)"),
237
+ },
238
+ }, async (input) => workflowToMcpContent(await sheetToCsvWorkflow({
239
+ ...(input.sheetsPayload !== undefined
240
+ ? { sheetsPayload: input.sheetsPayload }
241
+ : {}),
242
+ ...(input.rows !== undefined ? { rows: input.rows } : {}),
243
+ ...(input.spreadsheetId !== undefined
244
+ ? { spreadsheetId: input.spreadsheetId }
245
+ : {}),
246
+ ...(input.sheet !== undefined ? { sheet: input.sheet } : {}),
247
+ ...(input.writeCache !== undefined
248
+ ? { writeCache: input.writeCache }
249
+ : {}),
250
+ })));
251
+ server.registerTool("redmine_agent_batch_create_issues", {
252
+ description: "Feature → User Stories → Tasks from csvContent (from redmine_agent_sheet_to_csv) or csvPath. First call: pick_project (no default project). Then pick_target_version → map_columns → confirm_mapping → fix_assignees → preview_gate → proceed. Pass sheetSource from sheet_to_csv when batch started from Google Sheets (agent uses it for write-back hints only). User must confirm before previewAction proceed.",
253
+ inputSchema: {
254
+ csvPath: z
255
+ .string()
256
+ .optional()
257
+ .describe("Workspace path to CSV (e.g. batch_creation.csv)"),
258
+ csvContent: z.string().optional().describe("Raw CSV text if not using csvPath"),
259
+ project: z
260
+ .string()
261
+ .optional()
262
+ .describe("Project id or 'id — name' from getProjects. Required before column mapping — first batch call returns pick_project if omitted."),
263
+ status: z.string().optional().describe('Default "New"'),
264
+ priority: z.string().optional().describe('Default "Normal"'),
265
+ skipNodeIds: z
266
+ .array(z.string())
267
+ .optional()
268
+ .describe('nodeIds to skip (e.g. "row-12-subtask")'),
269
+ rowFixes: z
270
+ .record(z.string(), z.object({
271
+ subject: z.string().optional(),
272
+ assignedTo: z.string().optional(),
273
+ project: z.string().optional(),
274
+ targetVersion: z.string().optional(),
275
+ }))
276
+ .optional()
277
+ .describe("Per-row overrides keyed by nodeId"),
278
+ confirmSave: z.enum(["yes", "no"]).optional(),
279
+ previewAction: z
280
+ .enum(["show_preview", "proceed", "cancel"])
281
+ .optional()
282
+ .describe("After preview_gate: show_preview = full line list; proceed = post; cancel = abort"),
283
+ sheetSource: z
284
+ .object({
285
+ spreadsheetId: z.string(),
286
+ sheetTitle: z.string(),
287
+ })
288
+ .optional()
289
+ .describe("From redmine_agent_sheet_to_csv — metadata for agent sheet write-back (Google Sheets MCP); not written by this tool"),
290
+ existingNodeIds: z
291
+ .record(z.string(), z.number())
292
+ .optional()
293
+ .describe("nodeId → Redmine issue id from a prior upload (feature-root, row-N-userStory); links parents without duplicating"),
294
+ assigneeNameFixes: z
295
+ .record(z.string(), z.string())
296
+ .optional()
297
+ .describe('Invalid sheet assignee label → member label (e.g. "Muzamil khan" → "1590 — Name"), "Leave unassigned" to create without assignee, or "Skip this row" to skip all rows with that label'),
298
+ priorityNameFixes: z
299
+ .record(z.string(), z.string())
300
+ .optional()
301
+ .describe('Invalid sheet priority label → priority from priorityOptions (e.g. "Highest" → "1 — Immediate")'),
302
+ trackerNameFixes: z
303
+ .record(z.string(), z.string())
304
+ .optional()
305
+ .describe('Invalid sheet Type label → tracker from trackerOptions (e.g. "CR" → "5 — Change Request")'),
306
+ missingTargetVersionFix: z
307
+ .string()
308
+ .optional()
309
+ .describe('Default target version for stories without Target Version in sheet: project version "id — name" or "Skip target version". Required after project, before column mapping — returns pick_target_version if omitted.'),
310
+ sheetParseMode: z
311
+ .enum(["structure_only"])
312
+ .optional()
313
+ .describe("structure_only: raw rows + fieldSchema for agent column mapping; then retry with columnMapping. Omitted without columnMapping also returns map_columns."),
314
+ columnMapping: z
315
+ .object({
316
+ headerRowIndex: z
317
+ .number()
318
+ .int()
319
+ .describe("0-based row index of header line in sheet"),
320
+ columns: z
321
+ .record(z.string(), z.union([z.string(), z.number()]))
322
+ .describe("Role → header label or 0-based column index (userStory, subtask, description, hours, targetVersion, assignedTo, comments, redmineId, …)"),
323
+ })
324
+ .optional()
325
+ .describe("Agent-provided mapping after reading detectedHeaders (required: userStory + subtask)"),
326
+ confirmMappingAction: z
327
+ .enum(["proceed"])
328
+ .optional()
329
+ .describe('After confirm_mapping: "proceed" to parse sheet and continue to preview_gate'),
330
+ },
331
+ }, async (input) => {
332
+ const batchInput = {};
333
+ if (input.sheetSource !== undefined) {
334
+ batchInput.sheetSource = input.sheetSource;
335
+ }
336
+ if (input.csvPath !== undefined)
337
+ batchInput.csvPath = input.csvPath;
338
+ if (input.csvContent !== undefined)
339
+ batchInput.csvContent = input.csvContent;
340
+ if (input.project !== undefined)
341
+ batchInput.project = input.project;
342
+ if (input.status !== undefined)
343
+ batchInput.status = input.status;
344
+ if (input.priority !== undefined)
345
+ batchInput.priority = input.priority;
346
+ if (input.skipNodeIds !== undefined)
347
+ batchInput.skipNodeIds = input.skipNodeIds;
348
+ if (input.confirmSave !== undefined)
349
+ batchInput.confirmSave = input.confirmSave;
350
+ if (input.previewAction !== undefined) {
351
+ batchInput.previewAction = input.previewAction;
352
+ }
353
+ if (input.existingNodeIds !== undefined) {
354
+ batchInput.existingNodeIds = input.existingNodeIds;
355
+ }
356
+ if (input.assigneeNameFixes !== undefined) {
357
+ batchInput.assigneeNameFixes = input.assigneeNameFixes;
358
+ }
359
+ if (input.priorityNameFixes !== undefined) {
360
+ batchInput.priorityNameFixes = input.priorityNameFixes;
361
+ }
362
+ if (input.trackerNameFixes !== undefined) {
363
+ batchInput.trackerNameFixes = input.trackerNameFixes;
364
+ }
365
+ if (input.missingTargetVersionFix !== undefined) {
366
+ batchInput.missingTargetVersionFix = input.missingTargetVersionFix;
367
+ }
368
+ if (input.sheetParseMode !== undefined) {
369
+ batchInput.sheetParseMode = input.sheetParseMode;
370
+ }
371
+ if (input.columnMapping !== undefined) {
372
+ batchInput.columnMapping = input.columnMapping;
373
+ }
374
+ if (input.confirmMappingAction !== undefined) {
375
+ batchInput.confirmMappingAction = input.confirmMappingAction;
376
+ }
377
+ if (input.rowFixes !== undefined) {
378
+ batchInput.rowFixes = {};
379
+ for (const [nodeId, fix] of Object.entries(input.rowFixes)) {
380
+ const entry = {};
381
+ if (fix.subject !== undefined)
382
+ entry.subject = fix.subject;
383
+ if (fix.assignedTo !== undefined)
384
+ entry.assignedTo = fix.assignedTo;
385
+ if (fix.project !== undefined)
386
+ entry.project = fix.project;
387
+ if (fix.targetVersion !== undefined) {
388
+ entry.targetVersion = fix.targetVersion;
389
+ }
390
+ batchInput.rowFixes[nodeId] = entry;
391
+ }
392
+ }
393
+ return workflowToMcpContent(await batchCreateIssuesWorkflow(batchInput));
394
+ });
395
+ server.registerTool("redmine_agent_list_permissions", {
396
+ description: "List effective Redmine permissions for the API key’s current user (union across their role permissions). Optionally filter by a single project ID.",
397
+ inputSchema: {
398
+ projectId: z.number().int().optional().describe("Filter by project ID"),
399
+ },
400
+ }, async (input) => {
401
+ const current = await getCurrentUserWithMemberships();
402
+ const user = current.user ?? current;
403
+ const memberships = (user.memberships ?? []);
404
+ const filteredMemberships = input?.projectId
405
+ ? memberships.filter((m) => m.project?.id === input.projectId)
406
+ : memberships;
407
+ const roleIds = new Set();
408
+ for (const m of filteredMemberships) {
409
+ for (const r of m.roles ?? [])
410
+ roleIds.add(r.id);
411
+ }
412
+ const permissionsUnion = new Set();
413
+ const rolesExpanded = [];
414
+ for (const roleId of [...roleIds].sort((a, b) => a - b)) {
415
+ try {
416
+ const role = (await getRolePermissions(roleId));
417
+ const permissions = (Array.isArray(role.permissions)
418
+ ? role.permissions
419
+ : []);
420
+ permissions.forEach((p) => permissionsUnion.add(p));
421
+ rolesExpanded.push({
422
+ roleId,
423
+ roleName: role.name ?? null,
424
+ projectNames: [
425
+ ...new Set(filteredMemberships
426
+ .filter((m) => (m.roles ?? []).some((r) => r.id === roleId))
427
+ .map((m) => m.project?.name)
428
+ .filter(Boolean)),
429
+ ],
430
+ permissions,
431
+ });
432
+ }
433
+ catch (e) {
434
+ const status = e?.response?.status;
435
+ rolesExpanded.push({
436
+ roleId,
437
+ roleName: null,
438
+ projectNames: [
439
+ ...new Set(filteredMemberships
440
+ .filter((m) => (m.roles ?? []).some((r) => r.id === roleId))
441
+ .map((m) => m.project?.name)
442
+ .filter(Boolean)),
443
+ ],
444
+ permissions: [],
445
+ fetchError: status ? `HTTP ${status}` : e.message,
446
+ });
447
+ }
448
+ }
449
+ return workflowToMcpContent({
450
+ success: true,
451
+ data: {
452
+ user: {
453
+ id: user.id,
454
+ login: user.login,
455
+ name: user.name ?? user.firstname,
456
+ },
457
+ memberships: filteredMemberships.map((m) => ({
458
+ project: m.project ?? null,
459
+ roles: (m.roles ?? []).map((r) => ({ id: r.id, name: r.name })),
460
+ })),
461
+ permissions: [...permissionsUnion].sort(),
462
+ permissionsCount: permissionsUnion.size,
463
+ rolesExpanded,
464
+ },
465
+ });
466
+ });
467
+ server.registerTool("redmine_agent_log_time", {
468
+ description: "Batch log time: dates → dateIssues → pairHours → pairActivities (from config cache) → pairComments → preview → confirmSave=yes. Activity options are preloaded at MCP connect; Step 4 requires activityBatchId from prior response.",
469
+ inputSchema: {
470
+ promptHint: z
471
+ .string()
472
+ .optional()
473
+ .describe("User message text; if it mentions 'today', date step is skipped"),
474
+ dates: z
475
+ .array(z.string())
476
+ .optional()
477
+ .describe("Step 1 — Today | Multiple | Other, or YYYY-MM-DD list"),
478
+ spentOn: z
479
+ .string()
480
+ .optional()
481
+ .describe("Custom date YYYY-MM-DD when Other was chosen"),
482
+ pendingDates: z.array(z.string()).optional(),
483
+ activeDate: z.string().optional(),
484
+ dateIssues: z
485
+ .record(z.string(), z.union([
486
+ z.array(z.number().int().positive()),
487
+ z.string(),
488
+ ]))
489
+ .optional()
490
+ .describe("Step 2 batch — issue IDs per date (number[] or comma-separated string)"),
491
+ pendingIssueIds: z.array(z.number().int().positive()).optional(),
492
+ activeIssueId: z.number().int().positive().optional(),
493
+ issueIds: z
494
+ .array(z.number().int().positive())
495
+ .optional()
496
+ .describe("Legacy — fills all dates if dateIssues omitted (single date only)"),
497
+ pairInclude: z
498
+ .enum(["yes", "no"])
499
+ .optional()
500
+ .describe("Legacy single pair; prefer pairIncludes"),
501
+ pairIncludes: z
502
+ .record(z.string(), z.string())
503
+ .optional()
504
+ .describe('Step 3 batch — all yes/no keyed by "YYYY-MM-DD:issueId"'),
505
+ pairHours: z
506
+ .record(z.string(), z.union([z.number(), z.string()]))
507
+ .optional()
508
+ .describe("Step 3 batch — hours per included pair key"),
509
+ pairActivities: z
510
+ .record(z.string(), z.string())
511
+ .optional()
512
+ .describe('Step 4 batch — activity per pair key: "default" or "id — name". Requires activityBatchId from prior Step 4 response.'),
513
+ activityBatchId: z
514
+ .string()
515
+ .optional()
516
+ .describe("Pass back from data.activityBatchId after user answers Step 4 activity AskQuestion (mandatory before pairActivities is accepted)"),
517
+ pairComments: z
518
+ .record(z.string(), z.string())
519
+ .optional()
520
+ .describe('Step 5 batch — "default" or custom text per pair key'),
521
+ issueSubjects: z
522
+ .record(z.string(), z.string())
523
+ .optional()
524
+ .describe("issueId → subject; pass back from data for question labels"),
525
+ issueOptions: z
526
+ .array(z.object({
527
+ issueId: z.number().int().positive(),
528
+ subject: z.string().optional(),
529
+ label: z.string().optional(),
530
+ }))
531
+ .optional(),
532
+ skippedPairs: z
533
+ .array(z.string())
534
+ .optional()
535
+ .describe('Skipped pair keys "YYYY-MM-DD:issueId"'),
536
+ issueId: z.number().int().positive().optional(),
537
+ hours: z.number().positive().optional().describe("Legacy single pair hours"),
538
+ commentChoice: z.enum(["default", "custom"]).optional(),
539
+ comments: z.string().optional(),
540
+ entries: z
541
+ .array(z.object({
542
+ issueId: z.number().int().positive(),
543
+ hours: z.number().positive(),
544
+ spentOn: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
545
+ comments: z.string(),
546
+ activityId: z.number().int().positive().optional(),
547
+ }))
548
+ .optional(),
549
+ confirmSave: z.enum(["yes", "no"]).optional(),
550
+ },
551
+ }, async (input) => workflowToMcpContent(await logTimeWorkflow({
552
+ ...(input.promptHint !== undefined
553
+ ? { promptHint: input.promptHint }
554
+ : {}),
555
+ ...(input.dates !== undefined ? { dates: input.dates } : {}),
556
+ ...(input.spentOn !== undefined
557
+ ? { spentOn: input.spentOn }
558
+ : {}),
559
+ ...(input.pendingDates !== undefined
560
+ ? { pendingDates: input.pendingDates }
561
+ : {}),
562
+ ...(input.activeDate !== undefined
563
+ ? { activeDate: input.activeDate }
564
+ : {}),
565
+ ...(input.dateIssues !== undefined
566
+ ? {
567
+ // Accept string/number[] in MCP schema; workflow normalizes this shape.
568
+ dateIssues: input.dateIssues,
569
+ }
570
+ : {}),
571
+ ...(input.pendingIssueIds !== undefined
572
+ ? { pendingIssueIds: input.pendingIssueIds }
573
+ : {}),
574
+ ...(input.activeIssueId !== undefined
575
+ ? { activeIssueId: input.activeIssueId }
576
+ : {}),
577
+ ...(input.issueIds !== undefined
578
+ ? { issueIds: input.issueIds }
579
+ : {}),
580
+ ...(input.issueId !== undefined
581
+ ? { issueId: input.issueId }
582
+ : {}),
583
+ ...(input.hours !== undefined ? { hours: input.hours } : {}),
584
+ ...(input.pairInclude !== undefined
585
+ ? { pairInclude: input.pairInclude }
586
+ : {}),
587
+ ...(input.pairIncludes !== undefined
588
+ ? { pairIncludes: input.pairIncludes }
589
+ : {}),
590
+ ...(input.pairHours !== undefined
591
+ ? { pairHours: input.pairHours }
592
+ : {}),
593
+ ...(input.pairActivities !== undefined
594
+ ? { pairActivities: input.pairActivities }
595
+ : {}),
596
+ ...(input.activityBatchId !== undefined
597
+ ? { activityBatchId: input.activityBatchId }
598
+ : {}),
599
+ ...(input.pairComments !== undefined
600
+ ? { pairComments: input.pairComments }
601
+ : {}),
602
+ ...(input.issueSubjects !== undefined
603
+ ? { issueSubjects: input.issueSubjects }
604
+ : {}),
605
+ ...(input.issueOptions !== undefined
606
+ ? { issueOptions: input.issueOptions }
607
+ : {}),
608
+ ...(input.skippedPairs !== undefined
609
+ ? { skippedPairs: input.skippedPairs }
610
+ : {}),
611
+ ...(input.commentChoice !== undefined
612
+ ? { commentChoice: input.commentChoice }
613
+ : {}),
614
+ ...(input.comments !== undefined
615
+ ? { comments: input.comments }
616
+ : {}),
617
+ ...(input.entries !== undefined
618
+ ? { entries: input.entries }
619
+ : {}),
620
+ ...(input.confirmSave !== undefined
621
+ ? { confirmSave: input.confirmSave }
622
+ : {}),
623
+ })));
624
+ }
625
+ function createMcpServer(config) {
626
+ const server = new McpServer({
627
+ name: "redmine-agent",
628
+ version: "3.9.0",
629
+ }, {
630
+ instructions: `${BASE_MCP_INSTRUCTIONS}\n\n${formatConfigForInstructions(config)}`,
631
+ });
632
+ registerTools(server, config);
633
+ return server;
634
+ }
154
635
  async function main() {
636
+ const config = await loadRedmineConfig();
637
+ const server = createMcpServer(config);
155
638
  const transport = new StdioServerTransport();
156
639
  await server.connect(transport);
157
640
  }