@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,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
+ }