@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.
- package/README.md +70 -0
- package/build/client/jira-client.js +721 -0
- package/build/handlers/board-handlers.js +326 -0
- package/build/handlers/filter-handlers.js +427 -0
- package/build/handlers/issue-handlers.js +311 -0
- package/build/handlers/project-handlers.js +368 -0
- package/build/handlers/resource-handlers.js +320 -0
- package/build/handlers/search-handlers.js +103 -0
- package/build/handlers/sprint-handlers.js +433 -0
- package/build/handlers/tool-resource-handlers.js +1185 -0
- package/build/health-check.js +67 -0
- package/build/index.js +141 -0
- package/build/schemas/request-schemas.js +187 -0
- package/build/schemas/tool-schemas.js +450 -0
- package/build/types/index.js +1 -0
- package/build/utils/formatters/base-formatter.js +58 -0
- package/build/utils/formatters/board-formatter.js +63 -0
- package/build/utils/formatters/filter-formatter.js +66 -0
- package/build/utils/formatters/index.js +7 -0
- package/build/utils/formatters/issue-formatter.js +84 -0
- package/build/utils/formatters/project-formatter.js +55 -0
- package/build/utils/formatters/search-formatter.js +62 -0
- package/build/utils/formatters/sprint-formatter.js +111 -0
- package/build/utils/text-processing.js +343 -0
- package/package.json +65 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { IssueFormatter } 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 === 'issue_key') {
|
|
9
|
+
normalized['issueKey'] = value;
|
|
10
|
+
}
|
|
11
|
+
else if (key === 'project_key') {
|
|
12
|
+
normalized['projectKey'] = value;
|
|
13
|
+
}
|
|
14
|
+
else if (key === 'issue_type') {
|
|
15
|
+
normalized['issueType'] = value;
|
|
16
|
+
}
|
|
17
|
+
else if (key === 'transition_id') {
|
|
18
|
+
normalized['transitionId'] = value;
|
|
19
|
+
}
|
|
20
|
+
else if (key === 'linked_issue_key') {
|
|
21
|
+
normalized['linkedIssueKey'] = value;
|
|
22
|
+
}
|
|
23
|
+
else if (key === 'link_type') {
|
|
24
|
+
normalized['linkType'] = value;
|
|
25
|
+
}
|
|
26
|
+
else if (key === 'custom_fields') {
|
|
27
|
+
normalized['customFields'] = value;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
normalized[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
// Validate the manage_jira_issue arguments
|
|
36
|
+
function validateManageJiraIssueArgs(args) {
|
|
37
|
+
if (typeof args !== 'object' || args === null) {
|
|
38
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_issue arguments: Expected an object with an operation parameter');
|
|
39
|
+
}
|
|
40
|
+
const normalizedArgs = normalizeArgs(args);
|
|
41
|
+
// Validate operation parameter
|
|
42
|
+
if (typeof normalizedArgs.operation !== 'string' ||
|
|
43
|
+
!['create', 'get', 'update', 'delete', 'transition', 'comment', 'link'].includes(normalizedArgs.operation)) {
|
|
44
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid operation parameter. Valid values are: create, get, update, delete, transition, comment, link');
|
|
45
|
+
}
|
|
46
|
+
// Validate parameters based on operation
|
|
47
|
+
switch (normalizedArgs.operation) {
|
|
48
|
+
case 'get':
|
|
49
|
+
if (typeof normalizedArgs.issueKey !== 'string' || normalizedArgs.issueKey.trim() === '') {
|
|
50
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueKey parameter. Please provide a valid issue key for the get operation.');
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case 'create':
|
|
54
|
+
if (typeof normalizedArgs.projectKey !== 'string' || normalizedArgs.projectKey.trim() === '') {
|
|
55
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid projectKey parameter. Please provide a valid project key for the create operation.');
|
|
56
|
+
}
|
|
57
|
+
if (typeof normalizedArgs.summary !== 'string' || normalizedArgs.summary.trim() === '') {
|
|
58
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid summary parameter. Please provide a valid summary for the create operation.');
|
|
59
|
+
}
|
|
60
|
+
if (typeof normalizedArgs.issueType !== 'string' || normalizedArgs.issueType.trim() === '') {
|
|
61
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueType parameter. Please provide a valid issue type for the create operation.');
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case 'update':
|
|
65
|
+
if (typeof normalizedArgs.issueKey !== 'string' || normalizedArgs.issueKey.trim() === '') {
|
|
66
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueKey parameter. Please provide a valid issue key for the update operation.');
|
|
67
|
+
}
|
|
68
|
+
// Ensure at least one update field is provided
|
|
69
|
+
if (normalizedArgs.summary === undefined &&
|
|
70
|
+
normalizedArgs.description === undefined &&
|
|
71
|
+
normalizedArgs.parent === undefined &&
|
|
72
|
+
normalizedArgs.assignee === undefined &&
|
|
73
|
+
normalizedArgs.priority === undefined &&
|
|
74
|
+
normalizedArgs.labels === undefined &&
|
|
75
|
+
normalizedArgs.customFields === undefined) {
|
|
76
|
+
throw new McpError(ErrorCode.InvalidParams, 'At least one update field (summary, description, parent, assignee, priority, labels, or customFields) must be provided for the update operation.');
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case 'delete':
|
|
80
|
+
if (typeof normalizedArgs.issueKey !== 'string' || normalizedArgs.issueKey.trim() === '') {
|
|
81
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueKey parameter. Please provide a valid issue key for the delete operation.');
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'transition':
|
|
85
|
+
if (typeof normalizedArgs.issueKey !== 'string' || normalizedArgs.issueKey.trim() === '') {
|
|
86
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueKey parameter. Please provide a valid issue key for the transition operation.');
|
|
87
|
+
}
|
|
88
|
+
if (typeof normalizedArgs.transitionId !== 'string' || normalizedArgs.transitionId.trim() === '') {
|
|
89
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid transitionId parameter. Please provide a valid transition ID for the transition operation.');
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case 'comment':
|
|
93
|
+
if (typeof normalizedArgs.issueKey !== 'string' || normalizedArgs.issueKey.trim() === '') {
|
|
94
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueKey parameter. Please provide a valid issue key for the comment operation.');
|
|
95
|
+
}
|
|
96
|
+
if (typeof normalizedArgs.comment !== 'string' || normalizedArgs.comment.trim() === '') {
|
|
97
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid comment parameter. Please provide a valid comment for the comment operation.');
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case 'link':
|
|
101
|
+
if (typeof normalizedArgs.issueKey !== 'string' || normalizedArgs.issueKey.trim() === '') {
|
|
102
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid issueKey parameter. Please provide a valid issue key for the link operation.');
|
|
103
|
+
}
|
|
104
|
+
if (typeof normalizedArgs.linkedIssueKey !== 'string' || normalizedArgs.linkedIssueKey.trim() === '') {
|
|
105
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid linkedIssueKey parameter. Please provide a valid linked issue key for the link operation.');
|
|
106
|
+
}
|
|
107
|
+
if (typeof normalizedArgs.linkType !== 'string' || normalizedArgs.linkType.trim() === '') {
|
|
108
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid linkType parameter. Please provide a valid link type for the link operation.');
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
// Validate expand parameter
|
|
113
|
+
if (normalizedArgs.expand !== undefined) {
|
|
114
|
+
if (!Array.isArray(normalizedArgs.expand)) {
|
|
115
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid expand parameter. Expected an array of strings.');
|
|
116
|
+
}
|
|
117
|
+
const validExpansions = ['comments', 'transitions', 'attachments', 'related_issues', 'history'];
|
|
118
|
+
for (const expansion of normalizedArgs.expand) {
|
|
119
|
+
if (typeof expansion !== 'string' || !validExpansions.includes(expansion)) {
|
|
120
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid expansion: ${expansion}. Valid expansions are: ${validExpansions.join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// Handler functions for each operation
|
|
127
|
+
async function handleGetIssue(jiraClient, args) {
|
|
128
|
+
// Parse expansion options
|
|
129
|
+
const expansionOptions = {};
|
|
130
|
+
if (args.expand) {
|
|
131
|
+
for (const expansion of args.expand) {
|
|
132
|
+
expansionOptions[expansion] = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Get issue with requested expansions
|
|
136
|
+
const includeComments = expansionOptions.comments || false;
|
|
137
|
+
const includeAttachments = expansionOptions.attachments || false;
|
|
138
|
+
const issue = await jiraClient.getIssue(args.issueKey, includeComments, includeAttachments);
|
|
139
|
+
// Get transitions if requested
|
|
140
|
+
let transitions = undefined;
|
|
141
|
+
if (expansionOptions.transitions) {
|
|
142
|
+
transitions = await jiraClient.getTransitions(args.issueKey);
|
|
143
|
+
}
|
|
144
|
+
// Format the response using the IssueFormatter
|
|
145
|
+
const formattedResponse = IssueFormatter.formatIssue(issue, expansionOptions, transitions);
|
|
146
|
+
return {
|
|
147
|
+
content: [
|
|
148
|
+
{
|
|
149
|
+
type: 'text',
|
|
150
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async function handleCreateIssue(jiraClient, args) {
|
|
156
|
+
const result = await jiraClient.createIssue({
|
|
157
|
+
projectKey: args.projectKey,
|
|
158
|
+
summary: args.summary,
|
|
159
|
+
issueType: args.issueType,
|
|
160
|
+
description: args.description,
|
|
161
|
+
priority: args.priority,
|
|
162
|
+
assignee: args.assignee,
|
|
163
|
+
labels: args.labels,
|
|
164
|
+
customFields: args.customFields
|
|
165
|
+
});
|
|
166
|
+
// Get the created issue to return
|
|
167
|
+
const createdIssue = await jiraClient.getIssue(result.key, false, false);
|
|
168
|
+
const formattedResponse = IssueFormatter.formatIssue(createdIssue);
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function handleUpdateIssue(jiraClient, args) {
|
|
179
|
+
await jiraClient.updateIssue(args.issueKey, args.summary, args.description, args.parent);
|
|
180
|
+
// Get the updated issue to return
|
|
181
|
+
const updatedIssue = await jiraClient.getIssue(args.issueKey, false, false);
|
|
182
|
+
const formattedResponse = IssueFormatter.formatIssue(updatedIssue);
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function handleDeleteIssue(_jiraClient, _args) {
|
|
193
|
+
// Note: This is a placeholder. The current JiraClient doesn't have a deleteIssue method.
|
|
194
|
+
// You would need to implement this in the JiraClient class.
|
|
195
|
+
throw new McpError(ErrorCode.InternalError, 'Delete issue operation is not yet implemented');
|
|
196
|
+
// When implemented, it would look something like this:
|
|
197
|
+
/*
|
|
198
|
+
await _jiraClient.deleteIssue(_args.issueKey!);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: 'text',
|
|
204
|
+
text: JSON.stringify({
|
|
205
|
+
success: true,
|
|
206
|
+
message: `Issue ${_args.issueKey} has been deleted successfully.`,
|
|
207
|
+
}, null, 2),
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
*/
|
|
212
|
+
}
|
|
213
|
+
async function handleTransitionIssue(jiraClient, args) {
|
|
214
|
+
await jiraClient.transitionIssue(args.issueKey, args.transitionId, args.comment);
|
|
215
|
+
// Get the updated issue to return
|
|
216
|
+
const updatedIssue = await jiraClient.getIssue(args.issueKey, false, false);
|
|
217
|
+
const formattedResponse = IssueFormatter.formatIssue(updatedIssue);
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function handleCommentIssue(jiraClient, args) {
|
|
228
|
+
await jiraClient.addComment(args.issueKey, args.comment);
|
|
229
|
+
// Get the updated issue with comments to return
|
|
230
|
+
const updatedIssue = await jiraClient.getIssue(args.issueKey, true, false);
|
|
231
|
+
const formattedResponse = IssueFormatter.formatIssue(updatedIssue, { comments: true });
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: 'text',
|
|
236
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
async function handleLinkIssue(jiraClient, args) {
|
|
242
|
+
console.error(`Linking issue ${args.issueKey} to ${args.linkedIssueKey} with type ${args.linkType}`);
|
|
243
|
+
// Link the issues
|
|
244
|
+
await jiraClient.linkIssues(args.issueKey, args.linkedIssueKey, args.linkType, args.comment);
|
|
245
|
+
// Get the updated issue to return
|
|
246
|
+
const updatedIssue = await jiraClient.getIssue(args.issueKey, false, false);
|
|
247
|
+
const formattedResponse = IssueFormatter.formatIssue(updatedIssue);
|
|
248
|
+
return {
|
|
249
|
+
content: [
|
|
250
|
+
{
|
|
251
|
+
type: 'text',
|
|
252
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// Main handler function
|
|
258
|
+
export async function setupIssueHandlers(server, jiraClient, request) {
|
|
259
|
+
console.error('Handling issue request...');
|
|
260
|
+
const { name } = request.params;
|
|
261
|
+
const args = request.params.arguments;
|
|
262
|
+
if (!args) {
|
|
263
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing arguments. Please provide the required parameters for this operation.');
|
|
264
|
+
}
|
|
265
|
+
// Handle the manage_jira_issue tool
|
|
266
|
+
if (name === 'manage_jira_issue') {
|
|
267
|
+
// Normalize arguments to support both snake_case and camelCase
|
|
268
|
+
const normalizedArgs = normalizeArgs(args);
|
|
269
|
+
// Validate arguments
|
|
270
|
+
if (!validateManageJiraIssueArgs(normalizedArgs)) {
|
|
271
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_issue arguments');
|
|
272
|
+
}
|
|
273
|
+
// Process the operation
|
|
274
|
+
switch (normalizedArgs.operation) {
|
|
275
|
+
case 'get': {
|
|
276
|
+
console.error('Processing get issue operation');
|
|
277
|
+
return await handleGetIssue(jiraClient, normalizedArgs);
|
|
278
|
+
}
|
|
279
|
+
case 'create': {
|
|
280
|
+
console.error('Processing create issue operation');
|
|
281
|
+
return await handleCreateIssue(jiraClient, normalizedArgs);
|
|
282
|
+
}
|
|
283
|
+
case 'update': {
|
|
284
|
+
console.error('Processing update issue operation');
|
|
285
|
+
return await handleUpdateIssue(jiraClient, normalizedArgs);
|
|
286
|
+
}
|
|
287
|
+
case 'delete': {
|
|
288
|
+
console.error('Processing delete issue operation');
|
|
289
|
+
return await handleDeleteIssue(jiraClient, normalizedArgs);
|
|
290
|
+
}
|
|
291
|
+
case 'transition': {
|
|
292
|
+
console.error('Processing transition issue operation');
|
|
293
|
+
return await handleTransitionIssue(jiraClient, normalizedArgs);
|
|
294
|
+
}
|
|
295
|
+
case 'comment': {
|
|
296
|
+
console.error('Processing comment issue operation');
|
|
297
|
+
return await handleCommentIssue(jiraClient, normalizedArgs);
|
|
298
|
+
}
|
|
299
|
+
case 'link': {
|
|
300
|
+
console.error('Processing link issue operation');
|
|
301
|
+
return await handleLinkIssue(jiraClient, normalizedArgs);
|
|
302
|
+
}
|
|
303
|
+
default: {
|
|
304
|
+
console.error(`Unknown operation: ${normalizedArgs.operation}`);
|
|
305
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown operation: ${normalizedArgs.operation}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
console.error(`Unknown tool requested: ${name}`);
|
|
310
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
311
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { ProjectFormatter } 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 === 'project_key') {
|
|
9
|
+
normalized['projectKey'] = value;
|
|
10
|
+
}
|
|
11
|
+
else if (key === 'include_status_counts') {
|
|
12
|
+
normalized['includeStatusCounts'] = 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 project management arguments
|
|
27
|
+
function validateManageJiraProjectArgs(args) {
|
|
28
|
+
if (typeof args !== 'object' || args === null) {
|
|
29
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_project 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'].includes(normalizedArgs.operation)) {
|
|
35
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid operation parameter. Valid values are: get, create, update, delete, list');
|
|
36
|
+
}
|
|
37
|
+
// Validate parameters based on operation
|
|
38
|
+
switch (normalizedArgs.operation) {
|
|
39
|
+
case 'get':
|
|
40
|
+
if (typeof normalizedArgs.projectKey !== 'string' || normalizedArgs.projectKey.trim() === '') {
|
|
41
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid projectKey parameter. Please provide a valid project key for the get operation.');
|
|
42
|
+
}
|
|
43
|
+
// Validate project key format (e.g., PROJ)
|
|
44
|
+
if (!/^[A-Z][A-Z0-9_]+$/.test(normalizedArgs.projectKey)) {
|
|
45
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid project key format. Expected format: PROJ`);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case 'create':
|
|
49
|
+
if (typeof normalizedArgs.name !== 'string' || normalizedArgs.name.trim() === '') {
|
|
50
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid name parameter. Please provide a valid project name for the create operation.');
|
|
51
|
+
}
|
|
52
|
+
if (typeof normalizedArgs.key !== 'string' || normalizedArgs.key.trim() === '') {
|
|
53
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid key parameter. Please provide a valid project key for the create operation.');
|
|
54
|
+
}
|
|
55
|
+
// Validate project key format (e.g., PROJ)
|
|
56
|
+
if (!/^[A-Z][A-Z0-9_]+$/.test(normalizedArgs.key)) {
|
|
57
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid project key format. Expected format: PROJ`);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case 'update':
|
|
61
|
+
if (typeof normalizedArgs.projectKey !== 'string' || normalizedArgs.projectKey.trim() === '') {
|
|
62
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid projectKey parameter. Please provide a valid project key for the update operation.');
|
|
63
|
+
}
|
|
64
|
+
// Validate project key format (e.g., PROJ)
|
|
65
|
+
if (!/^[A-Z][A-Z0-9_]+$/.test(normalizedArgs.projectKey)) {
|
|
66
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid project key format. Expected format: PROJ`);
|
|
67
|
+
}
|
|
68
|
+
// Ensure at least one update field is provided
|
|
69
|
+
if (normalizedArgs.name === undefined &&
|
|
70
|
+
normalizedArgs.description === undefined &&
|
|
71
|
+
normalizedArgs.lead === undefined) {
|
|
72
|
+
throw new McpError(ErrorCode.InvalidParams, 'At least one update field (name, description, or lead) must be provided for the update operation.');
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case 'delete':
|
|
76
|
+
if (typeof normalizedArgs.projectKey !== 'string' || normalizedArgs.projectKey.trim() === '') {
|
|
77
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid projectKey parameter. Please provide a valid project key for the delete operation.');
|
|
78
|
+
}
|
|
79
|
+
// Validate project key format (e.g., PROJ)
|
|
80
|
+
if (!/^[A-Z][A-Z0-9_]+$/.test(normalizedArgs.projectKey)) {
|
|
81
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid project key format. Expected format: PROJ`);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
// Validate expand parameter
|
|
86
|
+
if (normalizedArgs.expand !== undefined) {
|
|
87
|
+
if (!Array.isArray(normalizedArgs.expand)) {
|
|
88
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid expand parameter. Expected an array of strings.');
|
|
89
|
+
}
|
|
90
|
+
const validExpansions = ['boards', 'components', 'versions', 'recent_issues'];
|
|
91
|
+
for (const expansion of normalizedArgs.expand) {
|
|
92
|
+
if (typeof expansion !== 'string' || !validExpansions.includes(expansion)) {
|
|
93
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid expansion: ${expansion}. Valid expansions are: ${validExpansions.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Validate pagination parameters
|
|
98
|
+
if (normalizedArgs.startAt !== undefined && typeof normalizedArgs.startAt !== 'number') {
|
|
99
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid startAt parameter. Please provide a valid number.');
|
|
100
|
+
}
|
|
101
|
+
if (normalizedArgs.maxResults !== undefined && typeof normalizedArgs.maxResults !== 'number') {
|
|
102
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid maxResults parameter. Please provide a valid number.');
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
// Handler functions for each operation
|
|
107
|
+
async function handleGetProject(jiraClient, args) {
|
|
108
|
+
const projectKey = args.projectKey;
|
|
109
|
+
const includeStatusCounts = args.include_status_counts !== false; // Default to true
|
|
110
|
+
// Parse expansion options
|
|
111
|
+
const expansionOptions = {};
|
|
112
|
+
if (args.expand) {
|
|
113
|
+
for (const expansion of args.expand) {
|
|
114
|
+
expansionOptions[expansion] = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Get all projects and find the requested one
|
|
118
|
+
const projects = await jiraClient.listProjects();
|
|
119
|
+
const project = projects.find(p => p.key === projectKey);
|
|
120
|
+
if (!project) {
|
|
121
|
+
throw new McpError(ErrorCode.InvalidRequest, `Project not found: ${projectKey}`);
|
|
122
|
+
}
|
|
123
|
+
// Convert to ProjectData format
|
|
124
|
+
const projectData = {
|
|
125
|
+
id: project.id,
|
|
126
|
+
key: project.key,
|
|
127
|
+
name: project.name,
|
|
128
|
+
description: project.description,
|
|
129
|
+
lead: project.lead,
|
|
130
|
+
url: project.url
|
|
131
|
+
};
|
|
132
|
+
// If status counts are requested, get them
|
|
133
|
+
if (includeStatusCounts) {
|
|
134
|
+
try {
|
|
135
|
+
// Get issue counts by status for this project
|
|
136
|
+
const searchResult = await jiraClient.searchIssues(`project = ${projectKey}`, 0, 0);
|
|
137
|
+
// Count issues by status
|
|
138
|
+
const statusCounts = {};
|
|
139
|
+
for (const issue of searchResult.issues) {
|
|
140
|
+
const status = issue.status;
|
|
141
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
142
|
+
}
|
|
143
|
+
projectData.status_counts = statusCounts;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error(`Error getting status counts for project ${projectKey}:`, error);
|
|
147
|
+
// Continue even if status counts fail
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Handle expansions
|
|
151
|
+
if (expansionOptions.boards) {
|
|
152
|
+
try {
|
|
153
|
+
// Get boards for this project
|
|
154
|
+
const boards = await jiraClient.listBoards();
|
|
155
|
+
const projectBoards = boards.filter(board => board.location?.projectId === Number(project.id) ||
|
|
156
|
+
board.location?.projectName === project.name);
|
|
157
|
+
// Add boards to the response
|
|
158
|
+
projectData.boards = projectBoards;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error(`Error getting boards for project ${projectKey}:`, error);
|
|
162
|
+
// Continue even if boards fail
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (expansionOptions.recent_issues) {
|
|
166
|
+
try {
|
|
167
|
+
// Get recent issues for this project
|
|
168
|
+
const searchResult = await jiraClient.searchIssues(`project = ${projectKey} ORDER BY updated DESC`, 0, 5);
|
|
169
|
+
// Add recent issues to the response
|
|
170
|
+
projectData.recent_issues = searchResult.issues;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error(`Error getting recent issues for project ${projectKey}:`, error);
|
|
174
|
+
// Continue even if recent issues fail
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Format the response
|
|
178
|
+
const formattedResponse = ProjectFormatter.formatProject(projectData, expansionOptions);
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: 'text',
|
|
183
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function handleCreateProject(_jiraClient, _args) {
|
|
189
|
+
// Note: This is a placeholder. The current JiraClient doesn't have a createProject method.
|
|
190
|
+
// You would need to implement this in the JiraClient class.
|
|
191
|
+
throw new McpError(ErrorCode.InternalError, 'Create project operation is not yet implemented');
|
|
192
|
+
// When implemented, it would look something like this:
|
|
193
|
+
/*
|
|
194
|
+
const result = await _jiraClient.createProject({
|
|
195
|
+
key: _args.key!,
|
|
196
|
+
name: _args.name!,
|
|
197
|
+
description: _args.description,
|
|
198
|
+
lead: _args.lead
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Get the created project to return
|
|
202
|
+
const createdProject = await _jiraClient.getProject(result.key);
|
|
203
|
+
const formattedResponse = ProjectFormatter.formatProject(createdProject);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: 'text',
|
|
209
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
*/
|
|
214
|
+
}
|
|
215
|
+
async function handleUpdateProject(_jiraClient, _args) {
|
|
216
|
+
// Note: This is a placeholder. The current JiraClient doesn't have an updateProject method.
|
|
217
|
+
// You would need to implement this in the JiraClient class.
|
|
218
|
+
throw new McpError(ErrorCode.InternalError, 'Update project operation is not yet implemented');
|
|
219
|
+
// When implemented, it would look something like this:
|
|
220
|
+
/*
|
|
221
|
+
await _jiraClient.updateProject(
|
|
222
|
+
_args.projectKey!,
|
|
223
|
+
_args.name,
|
|
224
|
+
_args.description,
|
|
225
|
+
_args.lead
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Get the updated project to return
|
|
229
|
+
const updatedProject = await _jiraClient.getProject(_args.projectKey!);
|
|
230
|
+
const formattedResponse = ProjectFormatter.formatProject(updatedProject);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: 'text',
|
|
236
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
*/
|
|
241
|
+
}
|
|
242
|
+
async function handleDeleteProject(_jiraClient, _args) {
|
|
243
|
+
// Note: This is a placeholder. The current JiraClient doesn't have a deleteProject method.
|
|
244
|
+
// You would need to implement this in the JiraClient class.
|
|
245
|
+
throw new McpError(ErrorCode.InternalError, 'Delete project operation is not yet implemented');
|
|
246
|
+
// When implemented, it would look something like this:
|
|
247
|
+
/*
|
|
248
|
+
await _jiraClient.deleteProject(_args.projectKey!);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: 'text',
|
|
254
|
+
text: JSON.stringify({
|
|
255
|
+
success: true,
|
|
256
|
+
message: `Project ${_args.projectKey} has been deleted successfully.`,
|
|
257
|
+
}, null, 2),
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
*/
|
|
262
|
+
}
|
|
263
|
+
async function handleListProjects(jiraClient, args) {
|
|
264
|
+
// Set default pagination values
|
|
265
|
+
const startAt = args.startAt !== undefined ? args.startAt : 0;
|
|
266
|
+
const maxResults = args.maxResults !== undefined ? args.maxResults : 50;
|
|
267
|
+
const includeStatusCounts = args.include_status_counts === true;
|
|
268
|
+
// Get all projects
|
|
269
|
+
const projects = await jiraClient.listProjects();
|
|
270
|
+
// Apply pagination
|
|
271
|
+
const paginatedProjects = projects.slice(startAt, startAt + maxResults);
|
|
272
|
+
// Convert to ProjectData format
|
|
273
|
+
const projectDataList = paginatedProjects.map(project => ({
|
|
274
|
+
id: project.id,
|
|
275
|
+
key: project.key,
|
|
276
|
+
name: project.name,
|
|
277
|
+
description: project.description,
|
|
278
|
+
lead: project.lead,
|
|
279
|
+
url: project.url
|
|
280
|
+
}));
|
|
281
|
+
// If status counts are requested, get them for each project
|
|
282
|
+
if (includeStatusCounts) {
|
|
283
|
+
// This would be more efficient with a batch API call, but for now we'll do it sequentially
|
|
284
|
+
for (const project of projectDataList) {
|
|
285
|
+
try {
|
|
286
|
+
// Get issue counts by status for this project
|
|
287
|
+
const searchResult = await jiraClient.searchIssues(`project = ${project.key}`, 0, 0);
|
|
288
|
+
// Count issues by status
|
|
289
|
+
const statusCounts = {};
|
|
290
|
+
for (const issue of searchResult.issues) {
|
|
291
|
+
const status = issue.status;
|
|
292
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
293
|
+
}
|
|
294
|
+
project.status_counts = statusCounts;
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
console.error(`Error getting status counts for project ${project.key}:`, error);
|
|
298
|
+
// Continue with other projects even if one fails
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Format the response
|
|
303
|
+
const formattedProjects = projectDataList.map(project => ProjectFormatter.formatProject(project));
|
|
304
|
+
// Create a response with pagination metadata
|
|
305
|
+
const response = {
|
|
306
|
+
data: formattedProjects,
|
|
307
|
+
_metadata: {
|
|
308
|
+
pagination: {
|
|
309
|
+
startAt,
|
|
310
|
+
maxResults,
|
|
311
|
+
total: projects.length,
|
|
312
|
+
hasMore: startAt + maxResults < projects.length,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
return {
|
|
317
|
+
content: [
|
|
318
|
+
{
|
|
319
|
+
type: 'text',
|
|
320
|
+
text: JSON.stringify(response, null, 2),
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// Main handler function
|
|
326
|
+
export async function setupProjectHandlers(server, jiraClient, request) {
|
|
327
|
+
console.error('Handling project request...');
|
|
328
|
+
const { name } = request.params;
|
|
329
|
+
const args = request.params.arguments || {};
|
|
330
|
+
// Handle the consolidated project management tool
|
|
331
|
+
if (name === 'manage_jira_project') {
|
|
332
|
+
// Normalize arguments to support both snake_case and camelCase
|
|
333
|
+
const normalizedArgs = normalizeArgs(args);
|
|
334
|
+
// Validate arguments
|
|
335
|
+
if (!validateManageJiraProjectArgs(normalizedArgs)) {
|
|
336
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid manage_jira_project arguments');
|
|
337
|
+
}
|
|
338
|
+
// Process the operation
|
|
339
|
+
switch (normalizedArgs.operation) {
|
|
340
|
+
case 'get': {
|
|
341
|
+
console.error('Processing get project operation');
|
|
342
|
+
return await handleGetProject(jiraClient, normalizedArgs);
|
|
343
|
+
}
|
|
344
|
+
case 'create': {
|
|
345
|
+
console.error('Processing create project operation');
|
|
346
|
+
return await handleCreateProject(jiraClient, normalizedArgs);
|
|
347
|
+
}
|
|
348
|
+
case 'update': {
|
|
349
|
+
console.error('Processing update project operation');
|
|
350
|
+
return await handleUpdateProject(jiraClient, normalizedArgs);
|
|
351
|
+
}
|
|
352
|
+
case 'delete': {
|
|
353
|
+
console.error('Processing delete project operation');
|
|
354
|
+
return await handleDeleteProject(jiraClient, normalizedArgs);
|
|
355
|
+
}
|
|
356
|
+
case 'list': {
|
|
357
|
+
console.error('Processing list projects operation');
|
|
358
|
+
return await handleListProjects(jiraClient, normalizedArgs);
|
|
359
|
+
}
|
|
360
|
+
default: {
|
|
361
|
+
console.error(`Unknown operation: ${normalizedArgs.operation}`);
|
|
362
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown operation: ${normalizedArgs.operation}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
console.error(`Unknown tool requested: ${name}`);
|
|
367
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
368
|
+
}
|