@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,427 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { FilterFormatter, SearchFormatter } 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 === 'filter_id') {
9
+ normalized['filterId'] = value;
10
+ }
11
+ else if (key === 'share_permissions') {
12
+ normalized['sharePermissions'] = value;
13
+ }
14
+ else if (key === 'start_at') {
15
+ normalized['startAt'] = value;
16
+ }
17
+ else if (key === 'max_results') {
18
+ normalized['maxResults'] = value;
19
+ }
20
+ else {
21
+ normalized[key] = value;
22
+ }
23
+ }
24
+ return normalized;
25
+ }
26
+ // Validate the consolidated filter management arguments
27
+ function validateManageJiraFilterArgs(args) {
28
+ if (typeof args !== 'object' || args === null) {
29
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_filter arguments: Expected an object with an operation parameter');
30
+ }
31
+ const normalizedArgs = normalizeArgs(args);
32
+ // Validate operation parameter
33
+ if (typeof normalizedArgs.operation !== 'string' ||
34
+ !['get', 'create', 'update', 'delete', 'list', 'execute_filter', 'execute_jql'].includes(normalizedArgs.operation)) {
35
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid operation parameter. Valid values are: get, create, update, delete, list, execute_filter, execute_jql');
36
+ }
37
+ // Validate parameters based on operation
38
+ switch (normalizedArgs.operation) {
39
+ case 'get':
40
+ case 'update':
41
+ case 'delete':
42
+ case 'execute_filter':
43
+ if (typeof normalizedArgs.filterId !== 'string' || normalizedArgs.filterId.trim() === '') {
44
+ throw new McpError(ErrorCode.InvalidParams, `Missing or invalid filterId parameter. Please provide a valid filter ID for the ${normalizedArgs.operation} operation.`);
45
+ }
46
+ break;
47
+ case 'create':
48
+ if (typeof normalizedArgs.name !== 'string' || normalizedArgs.name.trim() === '') {
49
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid name parameter. Please provide a valid filter name for the create operation.');
50
+ }
51
+ if (typeof normalizedArgs.jql !== 'string' || normalizedArgs.jql.trim() === '') {
52
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid jql parameter. Please provide a valid JQL query for the create operation.');
53
+ }
54
+ break;
55
+ case 'execute_jql':
56
+ if (typeof normalizedArgs.jql !== 'string' || normalizedArgs.jql.trim() === '') {
57
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid jql parameter. Please provide a valid JQL query for the execute_jql operation.');
58
+ }
59
+ break;
60
+ }
61
+ // Validate pagination parameters for list and execute_jql operations
62
+ if (normalizedArgs.operation === 'list' || normalizedArgs.operation === 'execute_jql') {
63
+ if (normalizedArgs.startAt !== undefined && typeof normalizedArgs.startAt !== 'number') {
64
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid startAt parameter. Please provide a valid number.');
65
+ }
66
+ if (normalizedArgs.maxResults !== undefined && typeof normalizedArgs.maxResults !== 'number') {
67
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid maxResults parameter. Please provide a valid number.');
68
+ }
69
+ }
70
+ // Validate expand parameter
71
+ if (normalizedArgs.expand !== undefined) {
72
+ if (!Array.isArray(normalizedArgs.expand)) {
73
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid expand parameter. Expected an array of strings.');
74
+ }
75
+ // Combined list of valid expansions for both filter and search operations
76
+ const validExpansions = [
77
+ 'jql', 'description', 'permissions', 'issue_count', // Filter expansions
78
+ 'issue_details', 'transitions', 'comments_preview' // Search expansions
79
+ ];
80
+ for (const expansion of normalizedArgs.expand) {
81
+ if (typeof expansion !== 'string' || !validExpansions.includes(expansion)) {
82
+ throw new McpError(ErrorCode.InvalidParams, `Invalid expansion: ${expansion}. Valid expansions are: ${validExpansions.join(', ')}`);
83
+ }
84
+ }
85
+ }
86
+ // Validate sharePermissions parameter
87
+ if (normalizedArgs.sharePermissions !== undefined) {
88
+ if (!Array.isArray(normalizedArgs.sharePermissions)) {
89
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid sharePermissions parameter. Expected an array of permission objects.');
90
+ }
91
+ for (const permission of normalizedArgs.sharePermissions) {
92
+ if (typeof permission !== 'object' || permission === null) {
93
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid permission object in sharePermissions. Expected an object with a type property.');
94
+ }
95
+ if (typeof permission.type !== 'string' || !['group', 'project', 'global'].includes(permission.type)) {
96
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid permission type. Valid values are: group, project, global.');
97
+ }
98
+ if (permission.type === 'group' && (typeof permission.group !== 'string' || permission.group.trim() === '')) {
99
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid group parameter for group permission type.');
100
+ }
101
+ if (permission.type === 'project' && (typeof permission.project !== 'string' || permission.project.trim() === '')) {
102
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid project parameter for project permission type.');
103
+ }
104
+ }
105
+ }
106
+ return true;
107
+ }
108
+ // Handler functions for each operation
109
+ async function handleGetFilter(jiraClient, args) {
110
+ const filterId = args.filterId;
111
+ // Parse expansion options
112
+ const expansionOptions = {};
113
+ if (args.expand) {
114
+ for (const expansion of args.expand) {
115
+ if (['jql', 'description', 'permissions', 'issue_count'].includes(expansion)) {
116
+ expansionOptions[expansion] = true;
117
+ }
118
+ }
119
+ }
120
+ try {
121
+ // Get the filter by first getting its issues
122
+ // This is a workaround since we don't have direct access to the filter API
123
+ // The getFilterIssues method internally calls the filter API
124
+ await jiraClient.getFilterIssues(filterId);
125
+ // Now get the filter details from the list of all filters
126
+ const allFilters = await jiraClient.listMyFilters(expansionOptions.jql || expansionOptions.description || expansionOptions.permissions);
127
+ const filter = allFilters.find(f => f.id === filterId);
128
+ if (!filter) {
129
+ throw new McpError(ErrorCode.InvalidParams, `Filter with ID ${filterId} not found or not accessible`);
130
+ }
131
+ // Convert to FilterData format
132
+ const filterData = {
133
+ id: filter.id,
134
+ name: filter.name || 'Unnamed Filter',
135
+ owner: filter.owner || 'Unknown',
136
+ favourite: filter.favourite || false,
137
+ viewUrl: filter.viewUrl || '',
138
+ description: filter.description || '',
139
+ jql: filter.jql || '',
140
+ sharePermissions: filter.sharePermissions?.map(perm => ({
141
+ type: perm.type,
142
+ group: perm.group,
143
+ project: perm.project
144
+ })) || []
145
+ };
146
+ // Handle expansions
147
+ if (expansionOptions.issue_count) {
148
+ try {
149
+ // Get issue count for this filter
150
+ const issues = await jiraClient.getFilterIssues(filterId);
151
+ // Add issue count to the response
152
+ filterData.issueCount = issues.length;
153
+ }
154
+ catch (error) {
155
+ console.error(`Error getting issue count for filter ${filterId}:`, error);
156
+ // Continue even if issue count fails
157
+ }
158
+ }
159
+ // Format the response
160
+ const formattedResponse = FilterFormatter.formatFilter(filterData, expansionOptions);
161
+ return {
162
+ content: [
163
+ {
164
+ type: 'text',
165
+ text: JSON.stringify(formattedResponse, null, 2),
166
+ },
167
+ ],
168
+ };
169
+ }
170
+ catch (error) {
171
+ if (error instanceof McpError) {
172
+ throw error;
173
+ }
174
+ console.error(`Error getting filter ${filterId}:`, error);
175
+ throw new McpError(ErrorCode.InternalError, `Failed to get filter: ${error instanceof Error ? error.message : String(error)}`);
176
+ }
177
+ }
178
+ async function handleListFilters(jiraClient, args) {
179
+ // Set default pagination values
180
+ const startAt = args.startAt !== undefined ? args.startAt : 0;
181
+ const maxResults = args.maxResults !== undefined ? args.maxResults : 50;
182
+ // Parse expansion options
183
+ const expansionOptions = {};
184
+ if (args.expand) {
185
+ for (const expansion of args.expand) {
186
+ if (['jql', 'description', 'permissions', 'issue_count'].includes(expansion)) {
187
+ expansionOptions[expansion] = true;
188
+ }
189
+ }
190
+ }
191
+ // Get all filters
192
+ const filters = await jiraClient.listMyFilters(expansionOptions.jql || expansionOptions.description || expansionOptions.permissions);
193
+ // Apply pagination
194
+ const paginatedFilters = filters.slice(startAt, startAt + maxResults);
195
+ // Convert to FilterData format
196
+ const filterDataList = paginatedFilters.map(filter => ({
197
+ ...filter
198
+ }));
199
+ // If issue count is requested, get it for each filter
200
+ if (expansionOptions.issue_count) {
201
+ // This would be more efficient with a batch API call, but for now we'll do it sequentially
202
+ for (const filter of filterDataList) {
203
+ try {
204
+ // Get issues for this filter
205
+ const issues = await jiraClient.getFilterIssues(filter.id);
206
+ // Add issue count to the filter data
207
+ filter.issueCount = issues.length;
208
+ }
209
+ catch (error) {
210
+ console.error(`Error getting issues for filter ${filter.id}:`, error);
211
+ // Continue with other filters even if one fails
212
+ }
213
+ }
214
+ }
215
+ // Format the response
216
+ const formattedFilters = filterDataList.map(filter => FilterFormatter.formatFilter(filter, expansionOptions));
217
+ // Create a response with pagination metadata
218
+ const response = {
219
+ data: formattedFilters,
220
+ _metadata: {
221
+ pagination: {
222
+ startAt,
223
+ maxResults,
224
+ total: filters.length,
225
+ hasMore: startAt + maxResults < filters.length,
226
+ },
227
+ },
228
+ };
229
+ return {
230
+ content: [
231
+ {
232
+ type: 'text',
233
+ text: JSON.stringify(response, null, 2),
234
+ },
235
+ ],
236
+ };
237
+ }
238
+ async function handleCreateFilter(_jiraClient, _args) {
239
+ // Note: This is a placeholder. The current JiraClient doesn't have a createFilter method.
240
+ // You would need to implement this in the JiraClient class.
241
+ throw new McpError(ErrorCode.InternalError, 'Create filter operation is not yet implemented');
242
+ // When implemented, it would look something like this:
243
+ /*
244
+ const result = await _jiraClient.createFilter({
245
+ name: _args.name!,
246
+ jql: _args.jql!,
247
+ description: _args.description,
248
+ favourite: _args.favourite,
249
+ sharePermissions: _args.sharePermissions
250
+ });
251
+
252
+ // Get the created filter to return
253
+ const createdFilter = await _jiraClient.getFilter(result.id);
254
+ const formattedResponse = FilterFormatter.formatFilter(createdFilter);
255
+
256
+ return {
257
+ content: [
258
+ {
259
+ type: 'text',
260
+ text: JSON.stringify(formattedResponse, null, 2),
261
+ },
262
+ ],
263
+ };
264
+ */
265
+ }
266
+ async function handleUpdateFilter(_jiraClient, _args) {
267
+ // Note: This is a placeholder. The current JiraClient doesn't have an updateFilter method.
268
+ // You would need to implement this in the JiraClient class.
269
+ throw new McpError(ErrorCode.InternalError, 'Update filter operation is not yet implemented');
270
+ // When implemented, it would look something like this:
271
+ /*
272
+ await _jiraClient.updateFilter(
273
+ _args.filterId!,
274
+ {
275
+ name: _args.name,
276
+ jql: _args.jql,
277
+ description: _args.description,
278
+ favourite: _args.favourite,
279
+ sharePermissions: _args.sharePermissions
280
+ }
281
+ );
282
+
283
+ // Get the updated filter to return
284
+ const updatedFilter = await _jiraClient.getFilter(_args.filterId!);
285
+ const formattedResponse = FilterFormatter.formatFilter(updatedFilter);
286
+
287
+ return {
288
+ content: [
289
+ {
290
+ type: 'text',
291
+ text: JSON.stringify(formattedResponse, null, 2),
292
+ },
293
+ ],
294
+ };
295
+ */
296
+ }
297
+ async function handleDeleteFilter(_jiraClient, _args) {
298
+ // Note: This is a placeholder. The current JiraClient doesn't have a deleteFilter method.
299
+ // You would need to implement this in the JiraClient class.
300
+ throw new McpError(ErrorCode.InternalError, 'Delete filter operation is not yet implemented');
301
+ // When implemented, it would look something like this:
302
+ /*
303
+ await _jiraClient.deleteFilter(_args.filterId!);
304
+
305
+ return {
306
+ content: [
307
+ {
308
+ type: 'text',
309
+ text: JSON.stringify({
310
+ success: true,
311
+ message: `Filter ${_args.filterId} has been deleted successfully.`,
312
+ }, null, 2),
313
+ },
314
+ ],
315
+ };
316
+ */
317
+ }
318
+ async function handleExecuteFilter(jiraClient, _args) {
319
+ const filterId = _args.filterId;
320
+ // Get issues for the filter
321
+ const issues = await jiraClient.getFilterIssues(filterId);
322
+ return {
323
+ content: [
324
+ {
325
+ type: 'text',
326
+ text: JSON.stringify({
327
+ data: issues,
328
+ _metadata: {
329
+ filter_id: filterId,
330
+ issue_count: issues.length
331
+ }
332
+ }, null, 2),
333
+ },
334
+ ],
335
+ };
336
+ }
337
+ async function handleExecuteJql(jiraClient, args) {
338
+ if (typeof args.jql !== 'string' || args.jql.trim() === '') {
339
+ throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid jql parameter. Please provide a valid JQL query for the execute_jql operation.');
340
+ }
341
+ // Set default pagination values
342
+ const startAt = args.startAt !== undefined ? args.startAt : 0;
343
+ const maxResults = args.maxResults !== undefined ? args.maxResults : 25;
344
+ try {
345
+ console.error(`Executing JQL search with args:`, JSON.stringify(args, null, 2));
346
+ // Parse search expansion options
347
+ const searchExpansionOptions = {};
348
+ if (args.expand) {
349
+ for (const expansion of args.expand) {
350
+ if (['issue_details', 'transitions', 'comments_preview'].includes(expansion)) {
351
+ searchExpansionOptions[expansion] = true;
352
+ }
353
+ }
354
+ }
355
+ // Execute the search
356
+ const searchResult = await jiraClient.searchIssues(args.jql, startAt, maxResults);
357
+ // Format the response using the SearchFormatter for enhanced results
358
+ const formattedResponse = SearchFormatter.formatSearchResult(searchResult, searchExpansionOptions);
359
+ return {
360
+ content: [
361
+ {
362
+ type: 'text',
363
+ text: JSON.stringify(formattedResponse, null, 2),
364
+ },
365
+ ],
366
+ };
367
+ }
368
+ catch (error) {
369
+ console.error('Error in execute_jql:', error);
370
+ if (error instanceof Error) {
371
+ throw new McpError(ErrorCode.InvalidRequest, `Jira API error: ${error.message}`);
372
+ }
373
+ throw new McpError(ErrorCode.InvalidRequest, 'Failed to execute Jira search');
374
+ }
375
+ }
376
+ // Main handler function
377
+ export async function setupFilterHandlers(server, jiraClient, request) {
378
+ console.error('Handling filter request...');
379
+ const { name } = request.params;
380
+ const args = request.params.arguments || {};
381
+ // Handle the consolidated filter management tool
382
+ if (name === 'manage_jira_filter') {
383
+ // Normalize arguments to support both snake_case and camelCase
384
+ const normalizedArgs = normalizeArgs(args);
385
+ // Validate arguments
386
+ if (!validateManageJiraFilterArgs(normalizedArgs)) {
387
+ throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_filter arguments');
388
+ }
389
+ // Process the operation
390
+ switch (normalizedArgs.operation) {
391
+ case 'get': {
392
+ console.error('Processing get filter operation');
393
+ return await handleGetFilter(jiraClient, normalizedArgs);
394
+ }
395
+ case 'list': {
396
+ console.error('Processing list filters operation');
397
+ return await handleListFilters(jiraClient, normalizedArgs);
398
+ }
399
+ case 'create': {
400
+ console.error('Processing create filter operation');
401
+ return await handleCreateFilter(jiraClient, normalizedArgs);
402
+ }
403
+ case 'update': {
404
+ console.error('Processing update filter operation');
405
+ return await handleUpdateFilter(jiraClient, normalizedArgs);
406
+ }
407
+ case 'delete': {
408
+ console.error('Processing delete filter operation');
409
+ return await handleDeleteFilter(jiraClient, normalizedArgs);
410
+ }
411
+ case 'execute_filter': {
412
+ console.error('Processing execute filter operation');
413
+ return await handleExecuteFilter(jiraClient, normalizedArgs);
414
+ }
415
+ case 'execute_jql': {
416
+ console.error('Processing execute JQL operation');
417
+ return await handleExecuteJql(jiraClient, normalizedArgs);
418
+ }
419
+ default: {
420
+ console.error(`Unknown operation: ${normalizedArgs.operation}`);
421
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown operation: ${normalizedArgs.operation}`);
422
+ }
423
+ }
424
+ }
425
+ console.error(`Unknown tool requested: ${name}`);
426
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
427
+ }