@aaronsb/jira-cloud-mcp 0.2.7 → 0.3.1

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.
@@ -1,67 +1,45 @@
1
1
  export const toolSchemas = {
2
- // Filter Management API
3
2
  manage_jira_filter: {
4
3
  name: 'manage_jira_filter',
5
- description: 'Filter management with CRUD operations and issue retrieval',
4
+ description: 'Search for issues using JQL queries, or manage saved filters',
6
5
  inputSchema: {
7
6
  type: 'object',
8
7
  properties: {
9
8
  operation: {
10
9
  type: 'string',
11
10
  enum: ['get', 'create', 'update', 'delete', 'list', 'execute_filter', 'execute_jql'],
12
- description: 'Operation to perform on the filter',
11
+ description: 'Operation to perform',
13
12
  },
14
- // Parameters for get, update, delete, execute_filter operations
15
13
  filterId: {
16
14
  type: 'string',
17
- description: 'The ID of the filter. Required for get, update, delete, and execute_filter operations. Can also use snake_case "filter_id".',
15
+ description: 'Filter ID. Required for get, update, delete, execute_filter.',
18
16
  },
19
- // Parameters for create and update operations
20
17
  name: {
21
18
  type: 'string',
22
- description: 'Name of the filter. Required for create operation, optional for update.',
19
+ description: 'Filter name. Required for create.',
23
20
  },
24
21
  jql: {
25
22
  type: 'string',
26
- description: 'JQL query string. Supports a wide range of search patterns:\n\n' +
27
- '# Portfolio/Plans Queries\n' +
28
- '- Find child issues: issue in portfolioChildIssuesOf("PROJ-123")\n' +
29
- '- Combined portfolio search: issue in portfolioChildIssuesOf("PROJ-123") AND status = "In Progress"\n' +
30
- '- Multiple portfolios: issue in portfolioChildIssuesOf("PROJ-123") OR issue in portfolioChildIssuesOf("PROJ-456")\n\n' +
31
- '# Common Search Patterns\n' +
32
- '- Assigned issues: assignee = currentUser()\n' +
33
- '- Unassigned issues: assignee IS EMPTY\n' +
34
- '- Recent changes: status CHANGED AFTER -1w\n' +
35
- '- Multiple statuses: status IN ("In Progress", "Under Review", "Testing")\n' +
36
- '- Priority tasks: priority = High AND status = Open\n' +
37
- '- Component search: component = "User Interface" OR component = "API"\n\n' +
38
- '# Advanced Functions\n' +
39
- '- Sort results: ORDER BY created DESC\n' +
40
- '- Track changes: status WAS "Resolved" AND status = "Open"\n' +
41
- '- Team filters: assignee IN MEMBERSOF("developers")\n\n' +
42
- 'JQL supports complex combinations using AND, OR, NOT operators and parentheses for grouping. ' +
43
- 'All text values are case-sensitive and must be enclosed in quotes when they contain spaces.',
23
+ description: 'JQL query string. Required for create and execute_jql. Read jira://tools/manage_jira_filter/documentation for syntax examples.',
44
24
  },
45
25
  description: {
46
26
  type: 'string',
47
- description: 'Description of the filter. Optional for create/update.',
27
+ description: 'Filter description.',
48
28
  },
49
29
  favourite: {
50
30
  type: 'boolean',
51
- description: 'Whether to mark the filter as a favorite. Optional for create/update.',
31
+ description: 'Mark as favorite.',
52
32
  },
53
- // Parameters for list operation
54
33
  startAt: {
55
34
  type: 'integer',
56
- description: 'Index of the first filter to return (0-based). Used for list and execute_jql operations. Can also use snake_case "start_at".',
35
+ description: 'Pagination offset (0-based).',
57
36
  default: 0,
58
37
  },
59
38
  maxResults: {
60
39
  type: 'integer',
61
- description: 'Maximum number of filters or issues to return. Used for list and execute_jql operations. Can also use snake_case "max_results".',
40
+ description: 'Max items to return.',
62
41
  default: 50,
63
42
  },
64
- // Parameters for sharing
65
43
  sharePermissions: {
66
44
  type: 'array',
67
45
  items: {
@@ -70,381 +48,304 @@ export const toolSchemas = {
70
48
  type: {
71
49
  type: 'string',
72
50
  enum: ['group', 'project', 'global'],
73
- description: 'Type of share permission',
74
- },
75
- group: {
76
- type: 'string',
77
- description: 'Group name (required when type is "group")',
78
- },
79
- project: {
80
- type: 'string',
81
- description: 'Project key (required when type is "project")',
82
51
  },
52
+ group: { type: 'string' },
53
+ project: { type: 'string' },
83
54
  },
84
55
  required: ['type'],
85
56
  },
86
- description: 'Share permissions for the filter. Optional for create/update. Can also use snake_case "share_permissions".',
57
+ description: 'Share permissions for the filter.',
87
58
  },
88
- // Common expansion options
89
59
  expand: {
90
60
  type: 'array',
91
61
  items: {
92
62
  type: 'string',
93
63
  enum: ['jql', 'description', 'permissions', 'issue_count', 'issue_details', 'transitions', 'comments_preview'],
94
64
  },
95
- description: 'Optional fields to include in the response',
65
+ description: 'Additional fields to include in the response.',
96
66
  },
97
67
  },
98
68
  required: ['operation'],
99
69
  },
100
70
  },
101
- // Sprint Management API
102
71
  manage_jira_sprint: {
103
72
  name: 'manage_jira_sprint',
104
- description: 'Sprint management with CRUD operations and issue management',
73
+ description: 'Manage sprints: create, start, close, and assign issues to sprints',
105
74
  inputSchema: {
106
75
  type: 'object',
107
76
  properties: {
108
77
  operation: {
109
78
  type: 'string',
110
79
  enum: ['get', 'create', 'update', 'delete', 'list', 'manage_issues'],
111
- description: 'Operation to perform on the sprint',
80
+ description: 'Operation to perform',
112
81
  },
113
- // Parameters for get operation
114
82
  sprintId: {
115
83
  type: 'integer',
116
- description: 'The ID of the sprint. Required for get, update, delete, and manage_issues operations. Can also use snake_case "sprint_id".',
84
+ description: 'Sprint ID. Required for get, update, delete, manage_issues.',
117
85
  },
118
- // Parameters for create operation
119
86
  boardId: {
120
87
  type: 'integer',
121
- description: 'The ID of the board. Required for create and list operations. Can also use snake_case "board_id".',
88
+ description: 'Board ID. Required for create and list.',
122
89
  },
123
90
  name: {
124
91
  type: 'string',
125
- description: 'Name of the sprint. Required for create operation, optional for update.',
92
+ description: 'Sprint name. Required for create.',
126
93
  },
127
- // Common parameters for create and update
128
94
  startDate: {
129
95
  type: 'string',
130
- description: 'Start date for the sprint in ISO format (e.g., "2025-03-20T00:00:00.000Z"). Can also use snake_case "start_date".',
96
+ description: 'Start date in ISO format (e.g., "2025-03-20T00:00:00.000Z").',
131
97
  },
132
98
  endDate: {
133
99
  type: 'string',
134
- description: 'End date for the sprint in ISO format (e.g., "2025-04-03T00:00:00.000Z"). Can also use snake_case "end_date".',
100
+ description: 'End date in ISO format.',
135
101
  },
136
102
  goal: {
137
103
  type: 'string',
138
- description: 'Goal or objective for the sprint',
104
+ description: 'Sprint goal.',
139
105
  },
140
106
  state: {
141
107
  type: 'string',
142
108
  enum: ['future', 'active', 'closed'],
143
- description: 'Sprint state. Used for filtering in list operation or changing state in update operation.',
109
+ description: 'Sprint state. Filter for list, or set via update.',
144
110
  },
145
- // Parameters for list operation
146
111
  startAt: {
147
112
  type: 'integer',
148
- description: 'Index of the first sprint to return (0-based). Used for list operation. Can also use snake_case "start_at".',
113
+ description: 'Pagination offset (0-based).',
149
114
  default: 0,
150
115
  },
151
116
  maxResults: {
152
117
  type: 'integer',
153
- description: 'Maximum number of sprints to return. Used for list operation. Can also use snake_case "max_results".',
118
+ description: 'Max items to return.',
154
119
  default: 50,
155
120
  },
156
- // Parameters for manage_issues operation
157
121
  add: {
158
122
  type: 'array',
159
- items: {
160
- type: 'string',
161
- },
162
- description: 'Array of issue keys to add to the sprint. Used for manage_issues operation.',
123
+ items: { type: 'string' },
124
+ description: 'Issue keys to add to sprint (manage_issues).',
163
125
  },
164
126
  remove: {
165
127
  type: 'array',
166
- items: {
167
- type: 'string',
168
- },
169
- description: 'Array of issue keys to remove from the sprint. Used for manage_issues operation.',
128
+ items: { type: 'string' },
129
+ description: 'Issue keys to remove from sprint (manage_issues).',
170
130
  },
171
- // Common expansion options
172
131
  expand: {
173
132
  type: 'array',
174
133
  items: {
175
134
  type: 'string',
176
135
  enum: ['issues', 'report', 'board'],
177
136
  },
178
- description: 'Optional fields to include in the response',
137
+ description: 'Additional fields to include in the response.',
179
138
  },
180
139
  },
181
140
  required: ['operation'],
182
141
  },
183
142
  },
184
- // Issue Management API
185
143
  manage_jira_issue: {
186
144
  name: 'manage_jira_issue',
187
- description: 'Issue management with CRUD operations, transitions, comments, and linking',
145
+ description: 'Get, create, update, delete, move, transition, comment on, or link Jira issues',
188
146
  inputSchema: {
189
147
  type: 'object',
190
148
  properties: {
191
149
  operation: {
192
150
  type: 'string',
193
- enum: ['create', 'get', 'update', 'delete', 'transition', 'comment', 'link'],
194
- description: 'Operation to perform on the issue',
151
+ enum: ['create', 'get', 'update', 'delete', 'move', 'transition', 'comment', 'link'],
152
+ description: 'Operation to perform',
195
153
  },
196
- // Parameters for get, update, delete, transition, comment, and link operations
197
154
  issueKey: {
198
155
  type: 'string',
199
- description: 'The Jira issue key (e.g., WORK-123). Required for all operations except create. Can also use snake_case "issue_key".',
156
+ description: 'Issue key (e.g., PROJ-123). Required for all operations except create.',
200
157
  },
201
- // Parameters for create operation
202
158
  projectKey: {
203
159
  type: 'string',
204
- description: 'Project key (e.g., PROJ). Required for create operation. Can also use snake_case "project_key".',
160
+ description: 'Project key (e.g., PROJ). Required for create.',
205
161
  },
206
- // Common parameters for create and update
207
162
  summary: {
208
163
  type: 'string',
209
- description: 'Issue summary/title. Required for create, optional for update.',
164
+ description: 'Issue title. Required for create.',
210
165
  },
211
166
  description: {
212
167
  type: 'string',
213
- description: 'Detailed description of the issue. Optional for create/update.',
168
+ description: 'Issue description.',
214
169
  },
215
170
  issueType: {
216
171
  type: 'string',
217
- description: 'Type of issue (e.g., Story, Bug, Task). Required for create. Can also use snake_case "issue_type".',
172
+ description: 'Issue type (e.g., Story, Bug, Task). Required for create.',
218
173
  },
219
174
  priority: {
220
175
  type: 'string',
221
- description: 'Issue priority (e.g., High, Medium, Low). Optional for create/update.',
176
+ description: 'Priority (e.g., High, Medium, Low).',
222
177
  },
223
178
  assignee: {
224
179
  type: 'string',
225
- description: 'Username of the assignee. Optional for create/update.',
180
+ description: 'Atlassian accountId of the assignee.',
226
181
  },
227
182
  labels: {
228
183
  type: 'array',
229
- items: {
230
- type: 'string'
231
- },
232
- description: 'Array of labels to apply to the issue. Optional for create/update.',
184
+ items: { type: 'string' },
185
+ description: 'Labels to apply.',
233
186
  },
234
187
  customFields: {
235
188
  type: 'object',
236
- description: 'Custom field values as key-value pairs. Optional for create/update. Can also use snake_case "custom_fields".',
189
+ description: 'Custom field values as key-value pairs.',
237
190
  },
238
- // Parameters for update operation
239
191
  parent: {
240
192
  type: ['string', 'null'],
241
- description: 'The key of the parent issue (e.g., PROJ-123) or null to remove parent. Optional for update.',
193
+ description: 'Parent issue key (e.g., PROJ-100) or null to remove.',
242
194
  },
243
- // Parameters for transition operation
244
195
  transitionId: {
245
196
  type: 'string',
246
- description: 'The ID of the transition to perform. Required for transition operation. Can also use snake_case "transition_id".',
197
+ description: 'Transition ID. Required for transition. Use expand: ["transitions"] on get to discover IDs.',
247
198
  },
248
- // Parameters for comment and transition operations
249
199
  comment: {
250
200
  type: 'string',
251
- description: 'Comment text. Required for comment operation, optional for transition.',
201
+ description: 'Comment text. Required for comment, optional for transition.',
252
202
  },
253
- // Parameters for link operation
254
203
  linkType: {
255
204
  type: 'string',
256
- description: 'Type of link between issues (e.g., "relates to", "blocks"). Required for link operation. Can also use snake_case "link_type".',
205
+ description: 'Link type (e.g., "blocks", "relates to"). Required for link. Read jira://issue-link-types for valid types.',
257
206
  },
258
207
  linkedIssueKey: {
259
208
  type: 'string',
260
- description: 'The key of the issue to link to. Required for link operation. Can also use snake_case "linked_issue_key".',
209
+ description: 'Issue key to link to. Required for link.',
210
+ },
211
+ targetProjectKey: {
212
+ type: 'string',
213
+ description: 'Target project key for move (e.g., NEWPROJ). Required for move.',
214
+ },
215
+ targetIssueType: {
216
+ type: 'string',
217
+ description: 'Target issue type for move (e.g., Story, Bug). Required for move.',
261
218
  },
262
- // Common expansion options
263
219
  expand: {
264
220
  type: 'array',
265
221
  items: {
266
222
  type: 'string',
267
223
  enum: ['comments', 'transitions', 'attachments', 'related_issues', 'history'],
268
224
  },
269
- description: 'Optional fields to include in the response',
225
+ description: 'Additional fields to include in the response.',
270
226
  },
271
227
  },
272
228
  required: ['operation'],
273
229
  },
274
230
  },
275
- // Project Management API
276
231
  manage_jira_project: {
277
232
  name: 'manage_jira_project',
278
- description: 'Project management with CRUD operations and related data',
233
+ description: 'List projects or get project details including status counts',
279
234
  inputSchema: {
280
235
  type: 'object',
281
236
  properties: {
282
237
  operation: {
283
238
  type: 'string',
284
- enum: ['get', 'create', 'update', 'delete', 'list'],
285
- description: 'Operation to perform on the project',
239
+ enum: ['get', 'list'],
240
+ description: 'Operation to perform',
286
241
  },
287
- // Parameters for get, update, delete operations
288
242
  projectKey: {
289
243
  type: 'string',
290
- description: 'The Jira project key (e.g., PROJ). Required for get, update, and delete operations. Can also use snake_case "project_key".',
291
- },
292
- // Parameters for create operation
293
- name: {
294
- type: 'string',
295
- description: 'Name of the project. Required for create operation, optional for update.',
296
- },
297
- key: {
298
- type: 'string',
299
- description: 'Project key. Required for create operation.',
300
- },
301
- // Common parameters for create and update
302
- description: {
303
- type: 'string',
304
- description: 'Description of the project. Optional for create/update.',
305
- },
306
- lead: {
307
- type: 'string',
308
- description: 'Username of the project lead. Optional for create/update.',
244
+ description: 'Project key (e.g., PROJ). Required for get.',
309
245
  },
310
- // Parameters for list operation
311
246
  startAt: {
312
247
  type: 'integer',
313
- description: 'Index of the first project to return (0-based). Used for list operation. Can also use snake_case "start_at".',
248
+ description: 'Pagination offset (0-based).',
314
249
  default: 0,
315
250
  },
316
251
  maxResults: {
317
252
  type: 'integer',
318
- description: 'Maximum number of projects to return. Used for list operation. Can also use snake_case "max_results".',
253
+ description: 'Max items to return.',
319
254
  default: 50,
320
255
  },
321
- // Common expansion options
322
256
  expand: {
323
257
  type: 'array',
324
258
  items: {
325
259
  type: 'string',
326
260
  enum: ['boards', 'components', 'versions', 'recent_issues'],
327
261
  },
328
- description: 'Optional fields to include in the response',
262
+ description: 'Additional fields to include in the response.',
329
263
  },
330
264
  include_status_counts: {
331
265
  type: 'boolean',
332
- description: 'Whether to include issue counts by status',
266
+ description: 'Include issue counts by status.',
333
267
  default: true,
334
268
  },
335
269
  },
336
270
  required: ['operation'],
337
271
  },
338
272
  },
339
- // Board Management API
340
273
  manage_jira_board: {
341
274
  name: 'manage_jira_board',
342
- description: 'Board management with CRUD operations and related data',
275
+ description: 'List boards or get board details and configuration',
343
276
  inputSchema: {
344
277
  type: 'object',
345
278
  properties: {
346
279
  operation: {
347
280
  type: 'string',
348
- enum: ['get', 'list', 'create', 'update', 'delete', 'get_configuration'],
349
- description: 'Operation to perform on the board',
281
+ enum: ['get', 'list'],
282
+ description: 'Operation to perform',
350
283
  },
351
- // Parameters for get, update, delete, get_configuration operations
352
284
  boardId: {
353
285
  type: 'integer',
354
- description: 'The ID of the board. Required for get, update, delete, and get_configuration operations. Can also use snake_case "board_id".',
286
+ description: 'Board ID. Required for get.',
355
287
  },
356
- // Parameters for create operation
357
- name: {
358
- type: 'string',
359
- description: 'Name of the board. Required for create operation, optional for update.',
360
- },
361
- type: {
362
- type: 'string',
363
- enum: ['scrum', 'kanban'],
364
- description: 'Type of board. Required for create operation.',
365
- },
366
- projectKey: {
367
- type: 'string',
368
- description: 'Project key for the board. Required for create operation. Can also use snake_case "project_key".',
369
- },
370
- // Parameters for list operation
371
288
  startAt: {
372
289
  type: 'integer',
373
- description: 'Index of the first board to return (0-based). Used for list operation. Can also use snake_case "start_at".',
290
+ description: 'Pagination offset (0-based).',
374
291
  default: 0,
375
292
  },
376
293
  maxResults: {
377
294
  type: 'integer',
378
- description: 'Maximum number of boards to return. Used for list operation. Can also use snake_case "max_results".',
295
+ description: 'Max items to return.',
379
296
  default: 50,
380
297
  },
381
- // Common expansion options
382
298
  expand: {
383
299
  type: 'array',
384
300
  items: {
385
301
  type: 'string',
386
302
  enum: ['sprints', 'issues', 'configuration'],
387
303
  },
388
- description: 'Optional fields to include in the response',
304
+ description: 'Additional fields to include in the response.',
389
305
  },
390
306
  include_sprints: {
391
307
  type: 'boolean',
392
- description: 'Whether to include active sprints for each board (shorthand for expand: ["sprints"])',
308
+ description: 'Include active sprints (shorthand for expand: ["sprints"]).',
393
309
  default: false,
394
310
  },
395
311
  },
396
312
  required: ['operation'],
397
313
  },
398
314
  },
399
- // Enhanced Search API - will be deprecated after consolidation
400
- search_jira_issues: {
401
- name: 'search_jira_issues',
402
- description: 'Search for issues using JQL with enhanced results and optional expansions',
315
+ queue_jira_operations: {
316
+ name: 'queue_jira_operations',
317
+ description: 'Execute multiple Jira operations in a single call. Operations run sequentially with result references ($0.key) and per-operation error strategies (bail/continue).',
403
318
  inputSchema: {
404
319
  type: 'object',
405
320
  properties: {
406
- jql: {
407
- type: 'string',
408
- description: 'JQL query string. Supports a wide range of search patterns:\n\n' +
409
- '# Portfolio/Plans Queries\n' +
410
- '- Find child issues: issue in portfolioChildIssuesOf("PROJ-123")\n' +
411
- '- Combined portfolio search: issue in portfolioChildIssuesOf("PROJ-123") AND status = "In Progress"\n' +
412
- '- Multiple portfolios: issue in portfolioChildIssuesOf("PROJ-123") OR issue in portfolioChildIssuesOf("PROJ-456")\n\n' +
413
- '# Common Search Patterns\n' +
414
- '- Assigned issues: assignee = currentUser()\n' +
415
- '- Unassigned issues: assignee IS EMPTY\n' +
416
- '- Recent changes: status CHANGED AFTER -1w\n' +
417
- '- Multiple statuses: status IN ("In Progress", "Under Review", "Testing")\n' +
418
- '- Priority tasks: priority = High AND status = Open\n' +
419
- '- Component search: component = "User Interface" OR component = "API"\n\n' +
420
- '# Advanced Functions\n' +
421
- '- Sort results: ORDER BY created DESC\n' +
422
- '- Track changes: status WAS "Resolved" AND status = "Open"\n' +
423
- '- Team filters: assignee IN MEMBERSOF("developers")\n\n' +
424
- 'JQL supports complex combinations using AND, OR, NOT operators and parentheses for grouping. ' +
425
- 'All text values are case-sensitive and must be enclosed in quotes when they contain spaces.',
426
- },
427
- startAt: {
428
- type: 'number',
429
- description: 'Index of the first issue to return (0-based). Can also use snake_case "start_at".',
430
- default: 0,
431
- },
432
- maxResults: {
433
- type: 'number',
434
- description: 'Maximum number of issues to return (default: 25, max: 100). Can also use snake_case "max_results".',
435
- default: 25,
436
- maximum: 100,
437
- },
438
- expand: {
321
+ operations: {
439
322
  type: 'array',
440
323
  items: {
441
- type: 'string',
442
- enum: ['issue_details', 'transitions', 'comments_preview'],
324
+ type: 'object',
325
+ properties: {
326
+ tool: {
327
+ type: 'string',
328
+ enum: ['manage_jira_issue', 'manage_jira_filter', 'manage_jira_sprint', 'manage_jira_project', 'manage_jira_board'],
329
+ description: 'Which tool to call.',
330
+ },
331
+ args: {
332
+ type: 'object',
333
+ description: 'Arguments for the tool call. Use $N.field to reference results from earlier operations (e.g., $0.key for the issue key from operation 0).',
334
+ },
335
+ onError: {
336
+ type: 'string',
337
+ enum: ['bail', 'continue'],
338
+ description: 'Error strategy. bail (default): stop queue. continue: log error, proceed to next.',
339
+ default: 'bail',
340
+ },
341
+ },
342
+ required: ['tool', 'args'],
443
343
  },
444
- description: 'Optional fields to include in the response',
344
+ description: 'Ordered list of operations to execute (max 10).',
345
+ maxItems: 10,
445
346
  },
446
347
  },
447
- required: ['jql'],
348
+ required: ['operations'],
448
349
  },
449
350
  },
450
351
  };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Sliding-window tracker for bulk-destructive operations (ADR-202 §1.4).
3
+ *
4
+ * Detects when an LLM agent is calling destructive operations (delete, move)
5
+ * in rapid succession and deflects to Jira's bulk-operations UI.
6
+ */
7
+ const WINDOW_MS = 60_000; // 60-second sliding window
8
+ const DEFAULT_LIMIT = 3;
9
+ /** Session-level operation tracker (singleton) */
10
+ class BulkOperationGuard {
11
+ ops = [];
12
+ limit;
13
+ constructor() {
14
+ const envLimit = process.env.JIRA_BULK_DESTRUCTIVE_LIMIT;
15
+ this.limit = envLimit ? parseInt(envLimit, 10) : DEFAULT_LIMIT;
16
+ if (isNaN(this.limit) || this.limit < 1) {
17
+ this.limit = DEFAULT_LIMIT;
18
+ }
19
+ }
20
+ /** Prune entries outside the sliding window */
21
+ prune() {
22
+ const cutoff = Date.now() - WINDOW_MS;
23
+ this.ops = this.ops.filter(op => op.timestamp >= cutoff);
24
+ }
25
+ /**
26
+ * Check whether a destructive operation should be allowed.
27
+ * Returns `null` if allowed, or a deflection message if refused.
28
+ */
29
+ check(operation, issueKey, jiraHost) {
30
+ this.prune();
31
+ const recentCount = this.ops.length;
32
+ if (recentCount < this.limit) {
33
+ return null; // allowed
34
+ }
35
+ // Build deflection
36
+ const recentKeys = this.ops.map(op => op.issueKey);
37
+ recentKeys.push(issueKey);
38
+ const jql = `key in (${recentKeys.join(', ')})`;
39
+ const encodedJql = encodeURIComponent(jql);
40
+ const lines = [
41
+ `Bulk ${operation} is not supported through this tool — ${operation === 'delete' ? 'deleting' : 'moving'} ${recentKeys.length} issues in quick succession is best done with manual review.`,
42
+ '',
43
+ `**Your JQL query:** \`${jql}\``,
44
+ ];
45
+ if (jiraHost) {
46
+ const host = jiraHost.replace(/^https?:\/\//, '');
47
+ lines.push(`**Review in Jira:** https://${host}/issues/?jql=${encodedJql}`);
48
+ }
49
+ lines.push('');
50
+ lines.push(`From Jira's issue list, select the issues and use the bulk operations menu.`);
51
+ lines.push('');
52
+ lines.push(`To ${operation} a single issue, wait a moment and try again with one issue at a time.`);
53
+ return lines.join('\n');
54
+ }
55
+ /** Record a successful destructive operation */
56
+ record(operation, issueKey) {
57
+ this.ops.push({ operation, issueKey, timestamp: Date.now() });
58
+ }
59
+ /** How many more destructive ops are allowed in the current window */
60
+ remainingCapacity() {
61
+ this.prune();
62
+ return Math.max(0, this.limit - this.ops.length);
63
+ }
64
+ /** Reset for testing */
65
+ reset() {
66
+ this.ops = [];
67
+ }
68
+ /** Visible for testing */
69
+ getLimit() {
70
+ return this.limit;
71
+ }
72
+ }
73
+ /** Singleton instance — lives for the MCP server process lifetime */
74
+ export const bulkOperationGuard = new BulkOperationGuard();