@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,326 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { BoardFormatter } from '../utils/formatters/index.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 === 'board_id') {
9
+ normalized['boardId'] = value;
10
+ }
11
+ else if (key === 'include_sprints') {
12
+ normalized['includeSprints'] = value;
13
+ }
14
+ else if (key === 'project_key') {
15
+ normalized['projectKey'] = value;
16
+ }
17
+ else if (key === 'start_at') {
18
+ normalized['startAt'] = value;
19
+ }
20
+ else if (key === 'max_results') {
21
+ normalized['maxResults'] = value;
22
+ }
23
+ else {
24
+ normalized[key] = value;
25
+ }
26
+ }
27
+ return normalized;
28
+ }
29
+ // Validate the consolidated board management arguments
30
+ function validateManageJiraBoardArgs(args) {
31
+ if (typeof args !== 'object' || args === null) {
32
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_board arguments: Expected an object with an operation parameter');
33
+ }
34
+ const normalizedArgs = normalizeArgs(args);
35
+ // Validate operation parameter
36
+ if (typeof normalizedArgs.operation !== 'string' ||
37
+ !['get', 'list', 'create', 'update', 'delete', 'get_configuration'].includes(normalizedArgs.operation)) {
38
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid operation parameter. Valid values are: get, list, create, update, delete, get_configuration');
39
+ }
40
+ // Validate parameters based on operation
41
+ switch (normalizedArgs.operation) {
42
+ case 'get':
43
+ case 'update':
44
+ case 'delete':
45
+ case 'get_configuration':
46
+ if (typeof normalizedArgs.boardId !== 'number') {
47
+ throw new McpError(ErrorCode.InvalidParams, `Missing or invalid boardId parameter. Please provide a valid board ID as a number for the ${normalizedArgs.operation} operation.`);
48
+ }
49
+ break;
50
+ case 'create':
51
+ if (typeof normalizedArgs.name !== 'string' || normalizedArgs.name.trim() === '') {
52
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid name parameter. Please provide a valid board name for the create operation.');
53
+ }
54
+ if (typeof normalizedArgs.type !== 'string' || !['scrum', 'kanban'].includes(normalizedArgs.type)) {
55
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid type parameter. Please provide a valid board type (scrum or kanban) for the create operation.');
56
+ }
57
+ if (typeof normalizedArgs.projectKey !== 'string' || normalizedArgs.projectKey.trim() === '') {
58
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid projectKey parameter. Please provide a valid project key for the create operation.');
59
+ }
60
+ break;
61
+ }
62
+ // Validate pagination parameters for list operation
63
+ if (normalizedArgs.operation === 'list') {
64
+ if (normalizedArgs.startAt !== undefined && typeof normalizedArgs.startAt !== 'number') {
65
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid startAt parameter. Please provide a valid number.');
66
+ }
67
+ if (normalizedArgs.maxResults !== undefined && typeof normalizedArgs.maxResults !== 'number') {
68
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid maxResults parameter. Please provide a valid number.');
69
+ }
70
+ }
71
+ // Validate expand parameter
72
+ if (normalizedArgs.expand !== undefined) {
73
+ if (!Array.isArray(normalizedArgs.expand)) {
74
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid expand parameter. Expected an array of strings.');
75
+ }
76
+ const validExpansions = ['sprints', 'issues', 'configuration'];
77
+ for (const expansion of normalizedArgs.expand) {
78
+ if (typeof expansion !== 'string' || !validExpansions.includes(expansion)) {
79
+ throw new McpError(ErrorCode.InvalidParams, `Invalid expansion: ${expansion}. Valid expansions are: ${validExpansions.join(', ')}`);
80
+ }
81
+ }
82
+ }
83
+ // Validate include_sprints parameter
84
+ if (normalizedArgs.includeSprints !== undefined && typeof normalizedArgs.includeSprints !== 'boolean') {
85
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid include_sprints parameter. Expected a boolean value.');
86
+ }
87
+ return true;
88
+ }
89
+ // Handler functions for each operation
90
+ async function handleGetBoard(jiraClient, args) {
91
+ const boardId = args.boardId;
92
+ // Parse expansion options
93
+ const expansionOptions = {};
94
+ if (args.expand) {
95
+ for (const expansion of args.expand) {
96
+ expansionOptions[expansion] = true;
97
+ }
98
+ }
99
+ // If include_sprints is true, add sprints to expansions
100
+ if (args.include_sprints === true) {
101
+ expansionOptions.sprints = true;
102
+ }
103
+ // Get all boards and find the requested one
104
+ const boards = await jiraClient.listBoards();
105
+ const board = boards.find((b) => b.id === boardId);
106
+ if (!board) {
107
+ throw new McpError(ErrorCode.InvalidRequest, `Board not found: ${boardId}`);
108
+ }
109
+ // Convert to BoardData format
110
+ const boardData = {
111
+ ...board
112
+ };
113
+ // Handle expansions
114
+ if (expansionOptions.sprints) {
115
+ try {
116
+ // Get sprints for this board
117
+ const sprints = await jiraClient.listBoardSprints(boardId);
118
+ // Add sprints to the response
119
+ boardData.sprints = sprints;
120
+ }
121
+ catch (error) {
122
+ console.error(`Error getting sprints for board ${boardId}:`, error);
123
+ // Continue even if sprints fail
124
+ }
125
+ }
126
+ // Format the response
127
+ const formattedResponse = BoardFormatter.formatBoard(boardData, expansionOptions);
128
+ return {
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: JSON.stringify(formattedResponse, null, 2),
133
+ },
134
+ ],
135
+ };
136
+ }
137
+ async function handleListBoards(jiraClient, args) {
138
+ // Set default pagination values
139
+ const startAt = args.startAt !== undefined ? args.startAt : 0;
140
+ const maxResults = args.maxResults !== undefined ? args.maxResults : 50;
141
+ const includeSprints = args.include_sprints === true;
142
+ // Get all boards
143
+ const boards = await jiraClient.listBoards();
144
+ // Apply pagination
145
+ const paginatedBoards = boards.slice(startAt, startAt + maxResults);
146
+ // Convert to BoardData format
147
+ const boardDataList = paginatedBoards.map(board => ({
148
+ ...board
149
+ }));
150
+ // If sprints are requested, get them for each board
151
+ if (includeSprints) {
152
+ // This would be more efficient with a batch API call, but for now we'll do it sequentially
153
+ for (const board of boardDataList) {
154
+ try {
155
+ // Get active sprints for this board
156
+ const sprints = await jiraClient.listBoardSprints(board.id);
157
+ // Add sprints to the board data
158
+ board.sprints = sprints;
159
+ }
160
+ catch (error) {
161
+ console.error(`Error getting sprints for board ${board.id}:`, error);
162
+ // Continue with other boards even if one fails
163
+ }
164
+ }
165
+ }
166
+ // Format the response
167
+ const formattedBoards = boardDataList.map(board => BoardFormatter.formatBoard(board, { sprints: includeSprints }));
168
+ // Create a response with pagination metadata
169
+ const response = {
170
+ data: formattedBoards,
171
+ _metadata: {
172
+ pagination: {
173
+ startAt,
174
+ maxResults,
175
+ total: boards.length,
176
+ hasMore: startAt + maxResults < boards.length,
177
+ },
178
+ },
179
+ };
180
+ return {
181
+ content: [
182
+ {
183
+ type: 'text',
184
+ text: JSON.stringify(response, null, 2),
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ async function handleCreateBoard(_jiraClient, _args) {
190
+ // Note: This is a placeholder. The current JiraClient doesn't have a createBoard method.
191
+ // You would need to implement this in the JiraClient class.
192
+ throw new McpError(ErrorCode.InternalError, 'Create board operation is not yet implemented');
193
+ // When implemented, it would look something like this:
194
+ /*
195
+ const result = await _jiraClient.createBoard({
196
+ name: _args.name!,
197
+ type: _args.type!,
198
+ projectKey: _args.projectKey!
199
+ });
200
+
201
+ // Get the created board to return
202
+ const createdBoard = await _jiraClient.getBoard(result.id);
203
+ const formattedResponse = BoardFormatter.formatBoard(createdBoard);
204
+
205
+ return {
206
+ content: [
207
+ {
208
+ type: 'text',
209
+ text: JSON.stringify(formattedResponse, null, 2),
210
+ },
211
+ ],
212
+ };
213
+ */
214
+ }
215
+ async function handleUpdateBoard(_jiraClient, _args) {
216
+ // Note: This is a placeholder. The current JiraClient doesn't have an updateBoard method.
217
+ // You would need to implement this in the JiraClient class.
218
+ throw new McpError(ErrorCode.InternalError, 'Update board operation is not yet implemented');
219
+ // When implemented, it would look something like this:
220
+ /*
221
+ await _jiraClient.updateBoard(
222
+ _args.boardId!,
223
+ _args.name
224
+ );
225
+
226
+ // Get the updated board to return
227
+ const updatedBoard = await _jiraClient.getBoard(_args.boardId!);
228
+ const formattedResponse = BoardFormatter.formatBoard(updatedBoard);
229
+
230
+ return {
231
+ content: [
232
+ {
233
+ type: 'text',
234
+ text: JSON.stringify(formattedResponse, null, 2),
235
+ },
236
+ ],
237
+ };
238
+ */
239
+ }
240
+ async function handleDeleteBoard(_jiraClient, _args) {
241
+ // Note: This is a placeholder. The current JiraClient doesn't have a deleteBoard method.
242
+ // You would need to implement this in the JiraClient class.
243
+ throw new McpError(ErrorCode.InternalError, 'Delete board operation is not yet implemented');
244
+ // When implemented, it would look something like this:
245
+ /*
246
+ await _jiraClient.deleteBoard(_args.boardId!);
247
+
248
+ return {
249
+ content: [
250
+ {
251
+ type: 'text',
252
+ text: JSON.stringify({
253
+ success: true,
254
+ message: `Board ${_args.boardId} has been deleted successfully.`,
255
+ }, null, 2),
256
+ },
257
+ ],
258
+ };
259
+ */
260
+ }
261
+ async function handleGetBoardConfiguration(_jiraClient, _args) {
262
+ // Note: This is a placeholder. The current JiraClient doesn't have a getBoardConfiguration method.
263
+ // You would need to implement this in the JiraClient class.
264
+ throw new McpError(ErrorCode.InternalError, 'Get board configuration operation is not yet implemented');
265
+ // When implemented, it would look something like this:
266
+ /*
267
+ const configuration = await _jiraClient.getBoardConfiguration(_args.boardId!);
268
+
269
+ return {
270
+ content: [
271
+ {
272
+ type: 'text',
273
+ text: JSON.stringify(configuration, null, 2),
274
+ },
275
+ ],
276
+ };
277
+ */
278
+ }
279
+ // Main handler function
280
+ export async function setupBoardHandlers(server, jiraClient, request) {
281
+ console.error('Handling board request...');
282
+ const { name } = request.params;
283
+ const args = request.params.arguments || {};
284
+ // Handle the consolidated board management tool
285
+ if (name === 'manage_jira_board') {
286
+ // Normalize arguments to support both snake_case and camelCase
287
+ const normalizedArgs = normalizeArgs(args);
288
+ // Validate arguments
289
+ if (!validateManageJiraBoardArgs(normalizedArgs)) {
290
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_board arguments');
291
+ }
292
+ // Process the operation
293
+ switch (normalizedArgs.operation) {
294
+ case 'get': {
295
+ console.error('Processing get board operation');
296
+ return await handleGetBoard(jiraClient, normalizedArgs);
297
+ }
298
+ case 'list': {
299
+ console.error('Processing list boards operation');
300
+ return await handleListBoards(jiraClient, normalizedArgs);
301
+ }
302
+ case 'create': {
303
+ console.error('Processing create board operation');
304
+ return await handleCreateBoard(jiraClient, normalizedArgs);
305
+ }
306
+ case 'update': {
307
+ console.error('Processing update board operation');
308
+ return await handleUpdateBoard(jiraClient, normalizedArgs);
309
+ }
310
+ case 'delete': {
311
+ console.error('Processing delete board operation');
312
+ return await handleDeleteBoard(jiraClient, normalizedArgs);
313
+ }
314
+ case 'get_configuration': {
315
+ console.error('Processing get board configuration operation');
316
+ return await handleGetBoardConfiguration(jiraClient, normalizedArgs);
317
+ }
318
+ default: {
319
+ console.error(`Unknown operation: ${normalizedArgs.operation}`);
320
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown operation: ${normalizedArgs.operation}`);
321
+ }
322
+ }
323
+ }
324
+ console.error(`Unknown tool requested: ${name}`);
325
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
326
+ }