@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,320 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { setupToolResourceHandlers } from './tool-resource-handlers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Sets up resource handlers for the Jira MCP server
|
|
5
|
+
* @param jiraClient The Jira client instance
|
|
6
|
+
* @returns Object containing resource handlers
|
|
7
|
+
*/
|
|
8
|
+
export function setupResourceHandlers(jiraClient) {
|
|
9
|
+
return {
|
|
10
|
+
/**
|
|
11
|
+
* Lists available static resources
|
|
12
|
+
*/
|
|
13
|
+
async listResources() {
|
|
14
|
+
// Get tool resources
|
|
15
|
+
const toolResourceHandler = setupToolResourceHandlers();
|
|
16
|
+
const toolResources = await toolResourceHandler.listToolResources();
|
|
17
|
+
return {
|
|
18
|
+
resources: [
|
|
19
|
+
{
|
|
20
|
+
uri: 'jira://instance/summary',
|
|
21
|
+
name: 'Jira Instance Summary',
|
|
22
|
+
mimeType: 'application/json',
|
|
23
|
+
description: 'High-level statistics about the Jira instance'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
uri: 'jira://projects/distribution',
|
|
27
|
+
name: 'Project Distribution',
|
|
28
|
+
mimeType: 'application/json',
|
|
29
|
+
description: 'Distribution of projects by type and status'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
uri: 'jira://issue-link-types',
|
|
33
|
+
name: 'Issue Link Types',
|
|
34
|
+
mimeType: 'application/json',
|
|
35
|
+
description: 'List of all available issue link types in the Jira instance'
|
|
36
|
+
},
|
|
37
|
+
// Add tool resources
|
|
38
|
+
...toolResources.resources
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Lists available resource templates
|
|
44
|
+
*/
|
|
45
|
+
async listResourceTemplates() {
|
|
46
|
+
return {
|
|
47
|
+
resourceTemplates: [
|
|
48
|
+
{
|
|
49
|
+
uriTemplate: 'jira://projects/{projectKey}/overview',
|
|
50
|
+
name: 'Project Overview',
|
|
51
|
+
mimeType: 'application/json',
|
|
52
|
+
description: 'Overview of a specific project including metadata and statistics'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
uriTemplate: 'jira://boards/{boardId}/overview',
|
|
56
|
+
name: 'Board Overview',
|
|
57
|
+
mimeType: 'application/json',
|
|
58
|
+
description: 'Overview of a specific board including sprints and statistics'
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Handles resource read requests
|
|
65
|
+
*/
|
|
66
|
+
async readResource(uri) {
|
|
67
|
+
console.error(`Reading resource: ${uri}`);
|
|
68
|
+
try {
|
|
69
|
+
// Handle static resources
|
|
70
|
+
if (uri === 'jira://instance/summary') {
|
|
71
|
+
return await getInstanceSummary(jiraClient);
|
|
72
|
+
}
|
|
73
|
+
if (uri === 'jira://projects/distribution') {
|
|
74
|
+
return await getProjectDistribution(jiraClient);
|
|
75
|
+
}
|
|
76
|
+
if (uri === 'jira://issue-link-types') {
|
|
77
|
+
return await getIssueLinkTypes(jiraClient);
|
|
78
|
+
}
|
|
79
|
+
// Handle resource templates
|
|
80
|
+
const projectMatch = uri.match(/^jira:\/\/projects\/([^/]+)\/overview$/);
|
|
81
|
+
if (projectMatch) {
|
|
82
|
+
const projectKey = projectMatch[1];
|
|
83
|
+
return await getProjectOverview(jiraClient, projectKey);
|
|
84
|
+
}
|
|
85
|
+
const boardMatch = uri.match(/^jira:\/\/boards\/(\d+)\/overview$/);
|
|
86
|
+
if (boardMatch) {
|
|
87
|
+
const boardId = parseInt(boardMatch[1], 10);
|
|
88
|
+
return await getBoardOverview(jiraClient, boardId);
|
|
89
|
+
}
|
|
90
|
+
// Handle tool resources
|
|
91
|
+
const toolMatch = uri.match(/^jira:\/\/tools\/([^/]+)\/documentation$/);
|
|
92
|
+
if (toolMatch) {
|
|
93
|
+
const toolResourceHandler = setupToolResourceHandlers();
|
|
94
|
+
return await toolResourceHandler.readToolResource(uri);
|
|
95
|
+
}
|
|
96
|
+
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error(`Error reading resource ${uri}:`, error);
|
|
100
|
+
if (error instanceof McpError) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
throw new McpError(ErrorCode.InternalError, `Error reading resource: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Gets a summary of the Jira instance
|
|
110
|
+
*/
|
|
111
|
+
async function getInstanceSummary(jiraClient) {
|
|
112
|
+
try {
|
|
113
|
+
// Get projects
|
|
114
|
+
const projects = await jiraClient.listProjects();
|
|
115
|
+
// Get boards
|
|
116
|
+
const boards = await jiraClient.listBoards();
|
|
117
|
+
// Get active sprints for each board (limited to first 5 boards for performance)
|
|
118
|
+
const sprints = (await Promise.all(boards.slice(0, 5).map(board => jiraClient.listBoardSprints(board.id)
|
|
119
|
+
.catch(() => []) // Ignore errors for individual boards
|
|
120
|
+
))).flat();
|
|
121
|
+
// Get project types distribution
|
|
122
|
+
const projectTypes = {};
|
|
123
|
+
projects.forEach(project => {
|
|
124
|
+
const projectType = project.key.includes('SD') ? 'service_desk' : 'software';
|
|
125
|
+
projectTypes[projectType] = (projectTypes[projectType] || 0) + 1;
|
|
126
|
+
});
|
|
127
|
+
// Format the response
|
|
128
|
+
const summary = {
|
|
129
|
+
totalProjects: projects.length,
|
|
130
|
+
totalBoards: boards.length,
|
|
131
|
+
activeSprintsCount: sprints.filter(s => s.state === 'active').length,
|
|
132
|
+
projectTypes,
|
|
133
|
+
recentActivity: {
|
|
134
|
+
timestamp: new Date().toISOString()
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
contents: [
|
|
139
|
+
{
|
|
140
|
+
uri: 'jira://instance/summary',
|
|
141
|
+
mimeType: 'application/json',
|
|
142
|
+
text: JSON.stringify(summary, null, 2)
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error('Error getting instance summary:', error);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Gets the distribution of projects
|
|
154
|
+
*/
|
|
155
|
+
async function getProjectDistribution(jiraClient) {
|
|
156
|
+
try {
|
|
157
|
+
// Get projects
|
|
158
|
+
const projects = await jiraClient.listProjects();
|
|
159
|
+
// Calculate distributions
|
|
160
|
+
const distribution = {
|
|
161
|
+
byType: {},
|
|
162
|
+
byLead: {},
|
|
163
|
+
total: projects.length
|
|
164
|
+
};
|
|
165
|
+
// Calculate type distribution
|
|
166
|
+
projects.forEach(project => {
|
|
167
|
+
const projectType = project.key.includes('SD') ? 'service_desk' : 'software';
|
|
168
|
+
distribution.byType[projectType] = (distribution.byType[projectType] || 0) + 1;
|
|
169
|
+
});
|
|
170
|
+
// Calculate lead distribution
|
|
171
|
+
projects.forEach(project => {
|
|
172
|
+
if (project.lead) {
|
|
173
|
+
distribution.byLead[project.lead] = (distribution.byLead[project.lead] || 0) + 1;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
contents: [
|
|
178
|
+
{
|
|
179
|
+
uri: 'jira://projects/distribution',
|
|
180
|
+
mimeType: 'application/json',
|
|
181
|
+
text: JSON.stringify(distribution, null, 2)
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
console.error('Error getting project distribution:', error);
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Gets an overview of a specific project
|
|
193
|
+
*/
|
|
194
|
+
async function getProjectOverview(jiraClient, projectKey) {
|
|
195
|
+
try {
|
|
196
|
+
// Get projects to find the one we want
|
|
197
|
+
const projects = await jiraClient.listProjects();
|
|
198
|
+
const project = projects.find(p => p.key === projectKey);
|
|
199
|
+
if (!project) {
|
|
200
|
+
throw new McpError(ErrorCode.InvalidRequest, `Project not found: ${projectKey}`);
|
|
201
|
+
}
|
|
202
|
+
// Get issues for this project (limited to 10 for performance)
|
|
203
|
+
const searchResponse = await jiraClient.searchIssues(`project = ${projectKey}`, 0, 10);
|
|
204
|
+
// Get status distribution
|
|
205
|
+
const statusDistribution = {};
|
|
206
|
+
searchResponse.issues.forEach(issue => {
|
|
207
|
+
if (issue.status) {
|
|
208
|
+
statusDistribution[issue.status] = (statusDistribution[issue.status] || 0) + 1;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Format the response
|
|
212
|
+
const overview = {
|
|
213
|
+
key: project.key,
|
|
214
|
+
name: project.name,
|
|
215
|
+
description: project.description,
|
|
216
|
+
lead: project.lead,
|
|
217
|
+
url: project.url,
|
|
218
|
+
issueCount: searchResponse.pagination.total,
|
|
219
|
+
statusDistribution,
|
|
220
|
+
recentIssues: searchResponse.issues.map(issue => ({
|
|
221
|
+
key: issue.key,
|
|
222
|
+
summary: issue.summary,
|
|
223
|
+
status: issue.status
|
|
224
|
+
}))
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
contents: [
|
|
228
|
+
{
|
|
229
|
+
uri: `jira://projects/${projectKey}/overview`,
|
|
230
|
+
mimeType: 'application/json',
|
|
231
|
+
text: JSON.stringify(overview, null, 2)
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
console.error(`Error getting project overview for ${projectKey}:`, error);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Gets an overview of a specific board
|
|
243
|
+
*/
|
|
244
|
+
async function getBoardOverview(jiraClient, boardId) {
|
|
245
|
+
try {
|
|
246
|
+
// Get boards to find the one we want
|
|
247
|
+
const boards = await jiraClient.listBoards();
|
|
248
|
+
const board = boards.find(b => b.id === boardId);
|
|
249
|
+
if (!board) {
|
|
250
|
+
throw new McpError(ErrorCode.InvalidRequest, `Board not found: ${boardId}`);
|
|
251
|
+
}
|
|
252
|
+
// Get sprints for this board
|
|
253
|
+
const sprints = await jiraClient.listBoardSprints(boardId);
|
|
254
|
+
// Format the response
|
|
255
|
+
const overview = {
|
|
256
|
+
id: board.id,
|
|
257
|
+
name: board.name,
|
|
258
|
+
type: board.type,
|
|
259
|
+
location: board.location,
|
|
260
|
+
sprints: sprints.map(sprint => ({
|
|
261
|
+
id: sprint.id,
|
|
262
|
+
name: sprint.name,
|
|
263
|
+
state: sprint.state,
|
|
264
|
+
startDate: sprint.startDate,
|
|
265
|
+
endDate: sprint.endDate,
|
|
266
|
+
goal: sprint.goal
|
|
267
|
+
}))
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
contents: [
|
|
271
|
+
{
|
|
272
|
+
uri: `jira://boards/${boardId}/overview`,
|
|
273
|
+
mimeType: 'application/json',
|
|
274
|
+
text: JSON.stringify(overview, null, 2)
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.error(`Error getting board overview for ${boardId}:`, error);
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Gets all available issue link types
|
|
286
|
+
*/
|
|
287
|
+
async function getIssueLinkTypes(jiraClient) {
|
|
288
|
+
try {
|
|
289
|
+
// Get all issue link types
|
|
290
|
+
const linkTypes = await jiraClient.getIssueLinkTypes();
|
|
291
|
+
// Format the response with usage examples
|
|
292
|
+
const formattedLinkTypes = linkTypes.map(linkType => ({
|
|
293
|
+
id: linkType.id,
|
|
294
|
+
name: linkType.name,
|
|
295
|
+
inward: linkType.inward,
|
|
296
|
+
outward: linkType.outward,
|
|
297
|
+
usage: {
|
|
298
|
+
description: `Use this link type to establish a "${linkType.outward}" relationship from one issue to another.`,
|
|
299
|
+
example: `When issue A ${linkType.outward} issue B, then issue B ${linkType.inward} issue A.`
|
|
300
|
+
}
|
|
301
|
+
}));
|
|
302
|
+
return {
|
|
303
|
+
contents: [
|
|
304
|
+
{
|
|
305
|
+
uri: 'jira://issue-link-types',
|
|
306
|
+
mimeType: 'application/json',
|
|
307
|
+
text: JSON.stringify({
|
|
308
|
+
linkTypes: formattedLinkTypes,
|
|
309
|
+
count: formattedLinkTypes.length,
|
|
310
|
+
timestamp: new Date().toISOString()
|
|
311
|
+
}, null, 2)
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
console.error('Error getting issue link types:', error);
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { 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 === 'start_at') {
|
|
9
|
+
normalized['startAt'] = value;
|
|
10
|
+
}
|
|
11
|
+
else if (key === 'max_results') {
|
|
12
|
+
normalized['maxResults'] = value;
|
|
13
|
+
}
|
|
14
|
+
else if (key === 'filter_id') {
|
|
15
|
+
normalized['filterId'] = value;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
normalized[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
function isSearchIssuesArgs(args) {
|
|
24
|
+
if (typeof args !== 'object' || args === null) {
|
|
25
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid search_jira_issues arguments: Expected an object with a jql parameter. Example: { "jql": "project = PROJ" }`);
|
|
26
|
+
}
|
|
27
|
+
const typedArgs = args;
|
|
28
|
+
if (typeof typedArgs.jql !== 'string') {
|
|
29
|
+
throw new McpError(ErrorCode.InvalidParams, `Missing or invalid jql parameter. Please provide a valid JQL query string. Example: { "jql": "project = PROJ" }`);
|
|
30
|
+
}
|
|
31
|
+
// Validate startAt if present
|
|
32
|
+
if (typedArgs.startAt !== undefined && typeof typedArgs.startAt !== 'number') {
|
|
33
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid startAt parameter. Expected a number.');
|
|
34
|
+
}
|
|
35
|
+
// Validate maxResults if present
|
|
36
|
+
if (typedArgs.maxResults !== undefined && typeof typedArgs.maxResults !== 'number') {
|
|
37
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid maxResults parameter. Expected a number.');
|
|
38
|
+
}
|
|
39
|
+
// Validate expand parameter if present
|
|
40
|
+
if (typedArgs.expand !== undefined) {
|
|
41
|
+
if (!Array.isArray(typedArgs.expand)) {
|
|
42
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid expand parameter. Expected an array of strings.');
|
|
43
|
+
}
|
|
44
|
+
const validExpansions = ['issue_details', 'transitions', 'comments_preview'];
|
|
45
|
+
for (const expansion of typedArgs.expand) {
|
|
46
|
+
if (typeof expansion !== 'string' || !validExpansions.includes(expansion)) {
|
|
47
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid expansion: ${expansion}. Valid expansions are: ${validExpansions.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export async function setupSearchHandlers(server, jiraClient, request) {
|
|
54
|
+
console.error('Handling search request...');
|
|
55
|
+
const { name } = request.params;
|
|
56
|
+
const args = request.params.arguments;
|
|
57
|
+
if (!args) {
|
|
58
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing arguments');
|
|
59
|
+
}
|
|
60
|
+
// Normalize arguments to support both snake_case and camelCase
|
|
61
|
+
const normalizedArgs = normalizeArgs(args);
|
|
62
|
+
switch (name) {
|
|
63
|
+
case 'search_jira_issues': {
|
|
64
|
+
console.error('Processing search_jira_issues request');
|
|
65
|
+
if (!isSearchIssuesArgs(normalizedArgs)) {
|
|
66
|
+
throw new McpError(ErrorCode.InvalidParams, 'Invalid search_jira_issues arguments');
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
console.error(`Executing search with args:`, JSON.stringify(normalizedArgs, null, 2));
|
|
70
|
+
// Parse expansion options
|
|
71
|
+
const expansionOptions = {};
|
|
72
|
+
if (normalizedArgs.expand) {
|
|
73
|
+
for (const expansion of normalizedArgs.expand) {
|
|
74
|
+
expansionOptions[expansion] = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Execute the search
|
|
78
|
+
const searchResult = await jiraClient.searchIssues(normalizedArgs.jql, normalizedArgs.startAt, normalizedArgs.maxResults);
|
|
79
|
+
// Format the response using the SearchFormatter
|
|
80
|
+
const formattedResponse = SearchFormatter.formatSearchResult(searchResult, expansionOptions);
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: 'text',
|
|
85
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Error in search_jira_issues:', error);
|
|
92
|
+
if (error instanceof Error) {
|
|
93
|
+
throw new McpError(ErrorCode.InvalidRequest, `Jira API error: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Failed to execute Jira search');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
default: {
|
|
99
|
+
console.error(`Unknown tool requested: ${name}`);
|
|
100
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|