@aaronsb/jira-cloud-mcp 0.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.
@@ -0,0 +1,433 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { SprintFormatter } from '../utils/formatters/sprint-formatter.js';
3
+ // Helper function to normalize parameter names (support both snake_case and camelCase)
4
+ function normalizeArgs(args) {
5
+ const normalized = {};
6
+ for (const [key, value] of Object.entries(args)) {
7
+ // Convert snake_case to camelCase
8
+ if (key === 'sprint_id') {
9
+ normalized['sprintId'] = value;
10
+ }
11
+ else if (key === 'board_id') {
12
+ normalized['boardId'] = value;
13
+ }
14
+ else if (key === 'start_date') {
15
+ normalized['startDate'] = value;
16
+ }
17
+ else if (key === 'end_date') {
18
+ normalized['endDate'] = value;
19
+ }
20
+ else if (key === 'max_results') {
21
+ normalized['maxResults'] = value;
22
+ }
23
+ else if (key === 'start_at') {
24
+ normalized['startAt'] = value;
25
+ }
26
+ else {
27
+ normalized[key] = value;
28
+ }
29
+ }
30
+ return normalized;
31
+ }
32
+ // Validate the consolidated sprint management arguments
33
+ function validateManageJiraSprintArgs(args) {
34
+ if (typeof args !== 'object' || args === null) {
35
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_sprint arguments: Expected an object with an operation parameter');
36
+ }
37
+ const normalizedArgs = normalizeArgs(args);
38
+ // Validate operation parameter
39
+ if (typeof normalizedArgs.operation !== 'string' ||
40
+ !['get', 'create', 'update', 'delete', 'list', 'manage_issues'].includes(normalizedArgs.operation)) {
41
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid operation parameter. Valid values are: get, create, update, delete, list, manage_issues');
42
+ }
43
+ // Validate parameters based on operation
44
+ switch (normalizedArgs.operation) {
45
+ case 'get':
46
+ if (typeof normalizedArgs.sprintId !== 'number') {
47
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid sprintId parameter. Please provide a valid sprint ID as a number for the get operation.');
48
+ }
49
+ break;
50
+ case 'create':
51
+ if (typeof normalizedArgs.boardId !== 'number') {
52
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid boardId parameter. Please provide a valid board ID as a number for the create operation.');
53
+ }
54
+ if (typeof normalizedArgs.name !== 'string' || normalizedArgs.name.trim() === '') {
55
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid name parameter. Please provide a valid sprint name as a string for the create operation.');
56
+ }
57
+ break;
58
+ case 'update':
59
+ if (typeof normalizedArgs.sprintId !== 'number') {
60
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid sprintId parameter. Please provide a valid sprint ID as a number for the update operation.');
61
+ }
62
+ // Ensure at least one update field is provided
63
+ if (normalizedArgs.name === undefined &&
64
+ normalizedArgs.goal === undefined &&
65
+ normalizedArgs.startDate === undefined &&
66
+ normalizedArgs.endDate === undefined &&
67
+ normalizedArgs.state === undefined) {
68
+ throw new McpError(ErrorCode.InvalidParams, 'At least one update field (name, goal, startDate, endDate, or state) must be provided for the update operation.');
69
+ }
70
+ break;
71
+ case 'delete':
72
+ if (typeof normalizedArgs.sprintId !== 'number') {
73
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid sprintId parameter. Please provide a valid sprint ID as a number for the delete operation.');
74
+ }
75
+ break;
76
+ case 'list':
77
+ if (typeof normalizedArgs.boardId !== 'number') {
78
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid boardId parameter. Please provide a valid board ID as a number for the list operation.');
79
+ }
80
+ break;
81
+ case 'manage_issues':
82
+ if (typeof normalizedArgs.sprintId !== 'number') {
83
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid sprintId parameter. Please provide a valid sprint ID as a number for the manage_issues operation.');
84
+ }
85
+ // Ensure at least one of add or remove is provided
86
+ if (!normalizedArgs.add && !normalizedArgs.remove) {
87
+ throw new McpError(ErrorCode.InvalidParams, 'At least one of add or remove must be provided for the manage_issues operation.');
88
+ }
89
+ break;
90
+ }
91
+ // Validate common optional parameters
92
+ if (normalizedArgs.startDate !== undefined && typeof normalizedArgs.startDate !== 'string') {
93
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid startDate parameter. Please provide a valid date string in ISO format.');
94
+ }
95
+ if (normalizedArgs.endDate !== undefined && typeof normalizedArgs.endDate !== 'string') {
96
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid endDate parameter. Please provide a valid date string in ISO format.');
97
+ }
98
+ if (normalizedArgs.goal !== undefined && typeof normalizedArgs.goal !== 'string') {
99
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid goal parameter. Please provide a valid goal as a string.');
100
+ }
101
+ if (normalizedArgs.state !== undefined) {
102
+ if (typeof normalizedArgs.state !== 'string' ||
103
+ !['future', 'active', 'closed'].includes(normalizedArgs.state)) {
104
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid state parameter. Valid values are: future, active, closed');
105
+ }
106
+ }
107
+ // Validate pagination parameters
108
+ if (normalizedArgs.startAt !== undefined && typeof normalizedArgs.startAt !== 'number') {
109
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid startAt parameter. Please provide a valid number.');
110
+ }
111
+ if (normalizedArgs.maxResults !== undefined && typeof normalizedArgs.maxResults !== 'number') {
112
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid maxResults parameter. Please provide a valid number.');
113
+ }
114
+ // Validate expand parameter
115
+ if (normalizedArgs.expand !== undefined) {
116
+ if (!Array.isArray(normalizedArgs.expand)) {
117
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid expand parameter. Expected an array of strings.');
118
+ }
119
+ const validExpansions = ['issues', 'report', 'board'];
120
+ for (const expansion of normalizedArgs.expand) {
121
+ if (typeof expansion !== 'string' || !validExpansions.includes(expansion)) {
122
+ throw new McpError(ErrorCode.InvalidParams, `Invalid expansion: ${expansion}. Valid expansions are: ${validExpansions.join(', ')}`);
123
+ }
124
+ }
125
+ }
126
+ // Validate add and remove parameters for manage_issues operation
127
+ if (normalizedArgs.add !== undefined) {
128
+ if (!Array.isArray(normalizedArgs.add)) {
129
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid add parameter. Expected an array of issue keys.');
130
+ }
131
+ for (const issueKey of normalizedArgs.add) {
132
+ if (typeof issueKey !== 'string') {
133
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid issue key in add parameter. All issue keys must be strings.');
134
+ }
135
+ }
136
+ }
137
+ if (normalizedArgs.remove !== undefined) {
138
+ if (!Array.isArray(normalizedArgs.remove)) {
139
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid remove parameter. Expected an array of issue keys.');
140
+ }
141
+ for (const issueKey of normalizedArgs.remove) {
142
+ if (typeof issueKey !== 'string') {
143
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid issue key in remove parameter. All issue keys must be strings.');
144
+ }
145
+ }
146
+ }
147
+ return true;
148
+ }
149
+ // Handler functions for each operation
150
+ async function handleGetSprint(jiraClient, args) {
151
+ // Parse expansion options
152
+ const expansionOptions = {};
153
+ if (args.expand) {
154
+ for (const expansion of args.expand) {
155
+ if (expansion === 'issues' || expansion === 'report') {
156
+ expansionOptions[expansion] = true;
157
+ }
158
+ }
159
+ }
160
+ // Get the sprint
161
+ const sprint = await jiraClient.getSprint(args.sprintId);
162
+ // Get issues if requested
163
+ let issues = undefined;
164
+ if (expansionOptions.issues) {
165
+ issues = await jiraClient.getSprintIssues(args.sprintId);
166
+ }
167
+ // Get report if requested
168
+ let report = undefined;
169
+ if (expansionOptions.report && sprint.state === 'closed') {
170
+ report = await jiraClient.getSprintReport(sprint.boardId, args.sprintId);
171
+ }
172
+ // Combine data
173
+ const sprintData = {
174
+ ...sprint,
175
+ issues,
176
+ report,
177
+ };
178
+ // Format the response
179
+ const formattedResponse = SprintFormatter.formatSprint(sprintData, expansionOptions);
180
+ return {
181
+ content: [
182
+ {
183
+ type: 'text',
184
+ text: JSON.stringify(formattedResponse, null, 2),
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ async function handleCreateSprint(jiraClient, args) {
190
+ // Create the sprint
191
+ const response = await jiraClient.createSprint(args.boardId, args.name, args.startDate, args.endDate, args.goal);
192
+ // Format the response
193
+ const formattedResponse = SprintFormatter.formatSprint(response);
194
+ return {
195
+ content: [
196
+ {
197
+ type: 'text',
198
+ text: JSON.stringify(formattedResponse, null, 2),
199
+ },
200
+ ],
201
+ };
202
+ }
203
+ async function handleUpdateSprint(jiraClient, args) {
204
+ try {
205
+ // Validate sprint state before updating
206
+ const currentSprint = await jiraClient.getSprint(args.sprintId);
207
+ // Check if trying to update a closed sprint
208
+ if (currentSprint.state === 'closed' && args.state !== 'closed') {
209
+ throw new McpError(ErrorCode.InvalidParams, 'Cannot update a closed sprint. Closed sprints are read-only.');
210
+ }
211
+ // Update the sprint
212
+ await jiraClient.updateSprint(args.sprintId, args.name, args.goal, args.startDate, args.endDate, args.state);
213
+ // Get the updated sprint
214
+ const updatedSprint = await jiraClient.getSprint(args.sprintId);
215
+ // Format the response
216
+ const formattedResponse = SprintFormatter.formatSprint(updatedSprint);
217
+ return {
218
+ content: [
219
+ {
220
+ type: 'text',
221
+ text: JSON.stringify(formattedResponse, null, 2),
222
+ },
223
+ ],
224
+ };
225
+ }
226
+ catch (error) {
227
+ console.error('Error updating sprint:', error);
228
+ // Provide more specific error messages based on the error type
229
+ if (error instanceof McpError) {
230
+ throw error; // Re-throw MCP errors
231
+ }
232
+ else if (error instanceof Error) {
233
+ throw new McpError(ErrorCode.InternalError, `Failed to update sprint: ${error.message}`);
234
+ }
235
+ else {
236
+ throw new McpError(ErrorCode.InternalError, 'An unknown error occurred while updating the sprint');
237
+ }
238
+ }
239
+ }
240
+ async function handleDeleteSprint(jiraClient, args) {
241
+ // Delete the sprint
242
+ await jiraClient.deleteSprint(args.sprintId);
243
+ return {
244
+ content: [
245
+ {
246
+ type: 'text',
247
+ text: JSON.stringify({
248
+ success: true,
249
+ message: `Sprint ${args.sprintId} has been deleted successfully.`,
250
+ }, null, 2),
251
+ },
252
+ ],
253
+ };
254
+ }
255
+ async function handleListSprints(jiraClient, args) {
256
+ // Set default pagination values
257
+ const startAt = args.startAt !== undefined ? args.startAt : 0;
258
+ const maxResults = args.maxResults !== undefined ? args.maxResults : 50;
259
+ // Get sprints
260
+ const response = await jiraClient.listSprints(args.boardId, args.state, startAt, maxResults);
261
+ // Format the response
262
+ const formattedResponse = SprintFormatter.formatSprintList(response.sprints, {
263
+ startAt,
264
+ maxResults,
265
+ total: response.total,
266
+ });
267
+ return {
268
+ content: [
269
+ {
270
+ type: 'text',
271
+ text: JSON.stringify(formattedResponse, null, 2),
272
+ },
273
+ ],
274
+ };
275
+ }
276
+ async function handleManageIssues(jiraClient, args) {
277
+ try {
278
+ // Validate sprint state before managing issues
279
+ const currentSprint = await jiraClient.getSprint(args.sprintId);
280
+ // Check if trying to modify a closed sprint
281
+ if (currentSprint.state === 'closed') {
282
+ throw new McpError(ErrorCode.InvalidParams, 'Cannot add or remove issues from a closed sprint. Closed sprints are read-only.');
283
+ }
284
+ // Validate that the issues exist before trying to add them
285
+ if (args.add && args.add.length > 0) {
286
+ // We could add validation here to check if issues exist
287
+ console.error(`Attempting to add ${args.add.length} issues to sprint ${args.sprintId}`);
288
+ }
289
+ // Update sprint issues
290
+ await jiraClient.updateSprintIssues(args.sprintId, args.add, args.remove);
291
+ // Get the updated sprint with issues
292
+ const sprint = await jiraClient.getSprint(args.sprintId);
293
+ const issues = await jiraClient.getSprintIssues(args.sprintId);
294
+ // Combine data
295
+ const sprintData = {
296
+ ...sprint,
297
+ issues,
298
+ };
299
+ // Format the response
300
+ const formattedResponse = SprintFormatter.formatSprint(sprintData, { issues: true });
301
+ return {
302
+ content: [
303
+ {
304
+ type: 'text',
305
+ text: JSON.stringify(formattedResponse, null, 2),
306
+ },
307
+ ],
308
+ };
309
+ }
310
+ catch (error) {
311
+ console.error('Error managing sprint issues:', error);
312
+ // Provide more specific error messages based on the error type
313
+ if (error instanceof McpError) {
314
+ throw error; // Re-throw MCP errors
315
+ }
316
+ else if (error instanceof Error) {
317
+ throw new McpError(ErrorCode.InternalError, `Failed to manage sprint issues: ${error.message}`);
318
+ }
319
+ else {
320
+ throw new McpError(ErrorCode.InternalError, 'An unknown error occurred while managing sprint issues');
321
+ }
322
+ }
323
+ }
324
+ // Legacy handler function for backward compatibility
325
+ async function handleLegacySprintTools(name, args, jiraClient) {
326
+ console.error(`Handling legacy sprint tool: ${name}`);
327
+ const normalizedArgs = normalizeArgs(args);
328
+ // Map legacy tool to consolidated tool operation
329
+ let operation;
330
+ if (name === 'create_jira_sprint') {
331
+ operation = 'create';
332
+ }
333
+ else if (name === 'get_jira_sprint') {
334
+ operation = 'get';
335
+ }
336
+ else if (name === 'list_jira_sprints') {
337
+ operation = 'list';
338
+ }
339
+ else if (name === 'update_jira_sprint') {
340
+ operation = 'update';
341
+ }
342
+ else if (name === 'delete_jira_sprint') {
343
+ operation = 'delete';
344
+ }
345
+ else if (name === 'update_sprint_issues') {
346
+ operation = 'manage_issues';
347
+ }
348
+ else {
349
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
350
+ }
351
+ // Create consolidated args
352
+ const consolidatedArgs = {
353
+ operation,
354
+ ...normalizedArgs
355
+ };
356
+ // Process the operation
357
+ switch (operation) {
358
+ case 'get':
359
+ return await handleGetSprint(jiraClient, consolidatedArgs);
360
+ case 'create':
361
+ return await handleCreateSprint(jiraClient, consolidatedArgs);
362
+ case 'update':
363
+ return await handleUpdateSprint(jiraClient, consolidatedArgs);
364
+ case 'delete':
365
+ return await handleDeleteSprint(jiraClient, consolidatedArgs);
366
+ case 'list':
367
+ return await handleListSprints(jiraClient, consolidatedArgs);
368
+ case 'manage_issues':
369
+ return await handleManageIssues(jiraClient, consolidatedArgs);
370
+ default:
371
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown operation: ${operation}`);
372
+ }
373
+ }
374
+ // Main handler function
375
+ export async function setupSprintHandlers(server, jiraClient, request) {
376
+ console.error('Handling sprint request...');
377
+ const { name } = request.params;
378
+ const args = request.params.arguments;
379
+ if (!args) {
380
+ throw new McpError(ErrorCode.InvalidParams, 'Missing arguments. Please provide the required parameters for this operation.');
381
+ }
382
+ // Handle legacy sprint tools for backward compatibility
383
+ if (name === 'create_jira_sprint' ||
384
+ name === 'get_jira_sprint' ||
385
+ name === 'list_jira_sprints' ||
386
+ name === 'update_jira_sprint' ||
387
+ name === 'delete_jira_sprint' ||
388
+ name === 'update_sprint_issues') {
389
+ return await handleLegacySprintTools(name, args, jiraClient);
390
+ }
391
+ // Handle the consolidated sprint management tool
392
+ if (name === 'manage_jira_sprint') {
393
+ // Normalize arguments to support both snake_case and camelCase
394
+ const normalizedArgs = normalizeArgs(args);
395
+ // Validate arguments
396
+ if (!validateManageJiraSprintArgs(normalizedArgs)) {
397
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_sprint arguments');
398
+ }
399
+ // Process the operation
400
+ switch (normalizedArgs.operation) {
401
+ case 'get': {
402
+ console.error('Processing get sprint operation');
403
+ return await handleGetSprint(jiraClient, normalizedArgs);
404
+ }
405
+ case 'create': {
406
+ console.error('Processing create sprint operation');
407
+ return await handleCreateSprint(jiraClient, normalizedArgs);
408
+ }
409
+ case 'update': {
410
+ console.error('Processing update sprint operation');
411
+ return await handleUpdateSprint(jiraClient, normalizedArgs);
412
+ }
413
+ case 'delete': {
414
+ console.error('Processing delete sprint operation');
415
+ return await handleDeleteSprint(jiraClient, normalizedArgs);
416
+ }
417
+ case 'list': {
418
+ console.error('Processing list sprints operation');
419
+ return await handleListSprints(jiraClient, normalizedArgs);
420
+ }
421
+ case 'manage_issues': {
422
+ console.error('Processing manage issues operation');
423
+ return await handleManageIssues(jiraClient, normalizedArgs);
424
+ }
425
+ default: {
426
+ console.error(`Unknown operation: ${normalizedArgs.operation}`);
427
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown operation: ${normalizedArgs.operation}`);
428
+ }
429
+ }
430
+ }
431
+ console.error(`Unknown tool requested: ${name}`);
432
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
433
+ }